perp-cli 0.12.17 → 0.14.1

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.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +303 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +248 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +925 -245
  103. package/dist/commands/history.js +102 -54
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +357 -459
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +240 -6
  166. package/dist/exchanges/aster.js +1139 -195
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +152 -0
  200. package/dist/exchanges/ondo/adapter.js +811 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +95 -0
  206. package/dist/exchanges/ondo/funds.js +198 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +293 -4
  218. package/dist/exchanges/pacifica.js +1044 -90
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1386 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +29 -0
  270. package/dist/exchanges/venues/ondo.js +136 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +39 -0
  274. package/dist/exchanges/venues/qfex.js +140 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +821 -403
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/onchain-balances.d.ts +40 -0
  293. package/dist/onchain-balances.js +173 -0
  294. package/dist/pacifica/client.d.ts +41 -10
  295. package/dist/pacifica/client.js +167 -63
  296. package/dist/pacifica/constants.d.ts +0 -1
  297. package/dist/pacifica/constants.js +0 -5
  298. package/dist/pacifica/deposit.d.ts +30 -3
  299. package/dist/pacifica/deposit.js +119 -2
  300. package/dist/pacifica/faucet.d.ts +29 -0
  301. package/dist/pacifica/faucet.js +100 -0
  302. package/dist/pacifica/sign-tx.d.ts +15 -0
  303. package/dist/pacifica/sign-tx.js +41 -0
  304. package/dist/pacifica/types/account.d.ts +28 -4
  305. package/dist/pacifica/types/lake.d.ts +20 -5
  306. package/dist/pacifica/types/lake.js +4 -0
  307. package/dist/pacifica/types/market.d.ts +5 -0
  308. package/dist/pacifica/types/order.d.ts +4 -1
  309. package/dist/pacifica/types/position.d.ts +9 -1
  310. package/dist/plan-executor.d.ts +10 -5
  311. package/dist/plan-executor.js +165 -54
  312. package/dist/position-history.js +13 -0
  313. package/dist/rebalance.d.ts +26 -2
  314. package/dist/rebalance.js +86 -7
  315. package/dist/relay-swap.d.ts +232 -0
  316. package/dist/relay-swap.js +630 -0
  317. package/dist/risk.js +5 -1
  318. package/dist/secret-env.d.ts +15 -0
  319. package/dist/secret-env.js +36 -0
  320. package/dist/settings.d.ts +169 -6
  321. package/dist/settings.js +219 -38
  322. package/dist/signer/ows-evm.d.ts +13 -0
  323. package/dist/signer/ows-evm.js +22 -0
  324. package/dist/smart-order.d.ts +9 -0
  325. package/dist/smart-order.js +53 -8
  326. package/dist/strategies/dca.d.ts +6 -2
  327. package/dist/strategies/dca.js +66 -26
  328. package/dist/strategies/funding-arb.d.ts +11 -1
  329. package/dist/strategies/funding-arb.js +202 -89
  330. package/dist/strategies/grid.d.ts +31 -0
  331. package/dist/strategies/grid.js +89 -11
  332. package/dist/strategies/pending-stop.d.ts +8 -0
  333. package/dist/strategies/pending-stop.js +12 -0
  334. package/dist/strategies/split-order.d.ts +45 -17
  335. package/dist/strategies/split-order.js +157 -91
  336. package/dist/strategies/trailing-stop.d.ts +2 -1
  337. package/dist/strategies/trailing-stop.js +52 -6
  338. package/dist/strategies/twap.d.ts +6 -1
  339. package/dist/strategies/twap.js +39 -11
  340. package/dist/trade-validator.d.ts +22 -0
  341. package/dist/trade-validator.js +119 -15
  342. package/dist/utils/numeric.d.ts +46 -0
  343. package/dist/utils/numeric.js +79 -0
  344. package/dist/utils.d.ts +19 -0
  345. package/dist/utils.js +96 -19
  346. package/dist/withdraw-gate.d.ts +49 -0
  347. package/dist/withdraw-gate.js +131 -0
  348. package/package.json +15 -6
  349. package/skills/perp-cli/SKILL.md +159 -55
  350. package/skills/perp-cli/references/agent-operations.md +112 -58
  351. package/skills/perp-cli/references/commands.md +269 -73
  352. package/skills/perp-cli/references/strategies.md +34 -31
  353. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  354. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  355. package/skills/perp-cli/scripts/preflight.sh +21 -14
  356. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  357. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  358. package/vendor/lighter-signer/SHA256SUMS +2 -0
  359. package/vendor/lighter-signer/VERSION +8 -0
  360. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  361. package/vendor/lighter-signer/wasm_exec.js +575 -0
  362. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  363. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -0,0 +1,1092 @@
1
+ /**
2
+ * Arcus adapter — native TradingV2 + legacy ExchangeAdapter shim (design §10.7).
3
+ *
4
+ * Signing: the agent API key is an Ed25519 key in OWS wallet
5
+ * `meta.agentWalletName` (raw signMessage on the Solana account — no prefix).
6
+ * Account = meta.ownerAddress, subaccount = meta.accountIndex. Without an agent
7
+ * (master creds only, for spot swaps) every perp private call throws
8
+ * AGENT_NOT_REGISTERED from requireAccount().
9
+ *
10
+ * Order entry rules (docs.arcus.xyz, design §10.10):
11
+ * - every order carries goodTilTime ≥ 1 month ahead (ARCUS_GOOD_TIL_HORIZON_MS),
12
+ * body in µs, signed `g` in ns;
13
+ * - market orders = IOC LIMIT at a protection price derived from mark and an
14
+ * explicit slippage bound, rejected locally when > 10 % from mark;
15
+ * - TIF (design §13.3): common GTC is sent as GTT with goodTil = now +
16
+ * ARCUS_GOOD_TIL_HORIZON_MS (recorded as execution.expiresAt); IOC / FOK / ALO
17
+ * are native; a limit order without a TIF is GTC;
18
+ * - clientId is always sent (orderAck async) and every OrderResult from a
19
+ * placement is `accepted` / `pending` unless the gateway already had state;
20
+ * - stop / TP-SL orders exist only as batchPlaceOrders groupings: a standalone
21
+ * stop is a 1-leg `partialTpsl` (reduce-only), TP/SL attached to an entry is
22
+ * an `entryTpsl` bundle.
23
+ */
24
+ import { PerpError } from "../../errors.js";
25
+ import { OwsEvmSigner } from "../../signer/ows-evm.js";
26
+ import { OwsSolanaSigner } from "../../signer/ows-solana.js";
27
+ import { alignProtectionPrice, floorSize, mulDecimal, requirePositive, slippagePrice, subDecimal, toUnitsExact, unitsToString } from "../precision.js";
28
+ import { newClientOrderId, unsupported, validateLegacyTif, validateOrderRequest, } from "../trading-v2.js";
29
+ import { networkConfig, requireCapabilities } from "../venue.js";
30
+ import { parseOrderRef, toLegacyMarketInfo, } from "../venue-types.js";
31
+ import { ArcusClient, arcusLeverageRejectError, arcusRejectionError } from "./client.js";
32
+ import { ed25519PubHexFromBase58 } from "./agent.js";
33
+ import { fetchArcusRawMarkets, toArcusVenueMarket } from "./public.js";
34
+ import { QUOTE_QUANTUM, assertArcusClientId, canonicalJson, cancelPayload, modifyPayload, nowNs, placePayload, signUtf8Hex, splitEvmSignature, withdrawTypedData, } from "./signing.js";
35
+ /**
36
+ * goodTilTime horizon. Arcus rejects anything nearer than one month at
37
+ * handling time (31 days covers every calendar month); the docs' examples use
38
+ * now + 40 days, which also leaves headroom for clock skew.
39
+ */
40
+ export const ARCUS_GOOD_TIL_HORIZON_MS = 40 * 86_400_000;
41
+ /** Arcus rejects MARKET protection prices more than 10 % from mark (also enforced locally for IOC limits). */
42
+ export const ARCUS_MAX_PROTECTION_DEVIATION = 0.10;
43
+ /** clientId max (36) minus the "-tp"/"-sl" suffix of entryTpsl child legs. */
44
+ export const ARCUS_TPSL_CLIENT_ID_MAX = 33;
45
+ /** Metadata (ids, ticks, steps) cache; prices are always re-fetched where they matter. */
46
+ const MARKETS_TTL_MS = 60_000;
47
+ const ORDERBOOK_LEVELS = 50;
48
+ const CLIENT_ID_HISTORY_SCAN = 200;
49
+ /** GET /v1/openOrders page cap (default = max = 1000). */
50
+ const OPEN_ORDERS_PAGE = 1000;
51
+ const CANDLE_TIMEFRAMES = new Set(["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w"]);
52
+ // ── Helpers ──
53
+ const STATUS_MAP = {
54
+ ACK: "accepted",
55
+ PENDING: "accepted",
56
+ TPSL_TRIGGERED: "accepted",
57
+ CANCEL_ACKNOWLEDGED: "accepted",
58
+ CANCEL_PENDING: "accepted",
59
+ OPEN: "open",
60
+ UNTRIGGERED: "open",
61
+ TPSL_PLACED: "open",
62
+ PARTIALLY_FILLED: "partiallyFilled",
63
+ FILLED: "filled",
64
+ LIQUIDATED: "filled",
65
+ ADL: "filled",
66
+ CANCELED: "cancelled",
67
+ MARGIN_CANCELED: "cancelled",
68
+ TPSL_CANCELED: "cancelled",
69
+ REJECTED: "rejected",
70
+ };
71
+ function mapStatus(s) {
72
+ const m = STATUS_MAP[s];
73
+ if (!m)
74
+ throw new PerpError("EXCHANGE_ERROR", `Arcus: unknown order status "${s}"`);
75
+ return m;
76
+ }
77
+ function finalityOf(s) {
78
+ return s === "filled" || s === "rejected" || s === "cancelled" ? "final" : "pending";
79
+ }
80
+ const usToMs = (us) => Math.floor(us / 1000);
81
+ function sideLower(s) { return s === "BUY" ? "buy" : "sell"; }
82
+ function tifFromRead(t) {
83
+ // Reads report resting (GTT) orders as the legacy "GTC".
84
+ switch (t) {
85
+ case "GTC":
86
+ case "GTT": return "GTT";
87
+ case "IOC": return "IOC";
88
+ case "FOK": return "FOK";
89
+ case "ALO": return "ALO";
90
+ default: throw new PerpError("EXCHANGE_ERROR", `Arcus: unknown timeInForce "${String(t)}" on order`);
91
+ }
92
+ }
93
+ /** Common / venue TIF → Arcus wire TIF. GTC (resting until cancelled) is sent as GTT at the max horizon. */
94
+ function toArcusTif(venue, t) {
95
+ if (t === "GTC")
96
+ return "GTT";
97
+ if (t === "GTT" || t === "IOC" || t === "FOK" || t === "ALO")
98
+ return t;
99
+ throw unsupported(venue.displayName, "timeInForce", `time-in-force ${t} is not offered`, "Use one of: GTC, IOC, FOK, ALO");
100
+ }
101
+ /** All-null execution record with the given method (fields set by the caller). */
102
+ function execution(method, partial) {
103
+ return {
104
+ method,
105
+ slippageBps: partial.slippageBps ?? null,
106
+ slippageSource: partial.slippageSource ?? null,
107
+ protectionPrice: partial.protectionPrice ?? null,
108
+ referencePrice: partial.referencePrice ?? null,
109
+ tifRequested: partial.tifRequested ?? null,
110
+ tifSent: partial.tifSent ?? null,
111
+ expiresAt: partial.expiresAt ?? null,
112
+ builderFee: partial.builderFee ?? null,
113
+ notes: partial.notes ?? [],
114
+ };
115
+ }
116
+ /** |protection − reference| in bps after tick alignment (the bound actually applied). */
117
+ function appliedBps(protection, ref) {
118
+ return Number((Math.abs(Number(protection) - Number(ref)) / Number(ref) * 10_000).toFixed(4));
119
+ }
120
+ /** goodTilTime (µs, decimal string) → unix ms; null when absent. */
121
+ function goodTilMs(us) {
122
+ return us === undefined ? null : Number(BigInt(us) / 1000n);
123
+ }
124
+ /** Execution record for an order read back from the venue (status reads). */
125
+ function readExecution(o) {
126
+ const method = o.tpslType ? (o.type === "LIMIT" ? "triggerLimit" : "trigger") : "limit";
127
+ const resting = o.timeInForce === "GTC" || o.timeInForce === "GTT" || o.timeInForce === "ALO";
128
+ return execution(method, {
129
+ tifSent: o.timeInForce === "GTC" ? "GTT" : (o.timeInForce ?? null),
130
+ expiresAt: resting ? goodTilMs(o.goodTilTime) : null,
131
+ referencePrice: o.triggerPrice ?? null,
132
+ });
133
+ }
134
+ /**
135
+ * Placement grid tick for a price: tickTiers band tick, or the top-level tickSize.
136
+ * `upToPrice` is an EXCLUSIVE upper bound (openapi MarketInfo.tickTiers), so a
137
+ * price equal to it belongs to the next band — hence `p < upToPrice`.
138
+ */
139
+ function tickForPrice(m, price) {
140
+ if (!m.tickTiers || m.tickTiers.length === 0)
141
+ return m.tickSize;
142
+ const p = Number(price);
143
+ for (const t of m.tickTiers) {
144
+ if (t.upToPrice === undefined || p < Number(t.upToPrice))
145
+ return t.tick;
146
+ }
147
+ return m.tickTiers[m.tickTiers.length - 1].tick;
148
+ }
149
+ /** goodTilTime (unix ms) for a placement made now: now + ARCUS_GOOD_TIL_HORIZON_MS. */
150
+ function goodTilAtMs() {
151
+ return Date.now() + ARCUS_GOOD_TIL_HORIZON_MS;
152
+ }
153
+ function isArcusMeta(a) {
154
+ return !!a && typeof a === "object" && a.kind === "arcus";
155
+ }
156
+ export class ArcusAdapter {
157
+ name = "arcus";
158
+ venue;
159
+ network;
160
+ caps;
161
+ client;
162
+ account;
163
+ masterCreds;
164
+ masterPk;
165
+ markets = null;
166
+ constructor(def, ctx) {
167
+ this.venue = def;
168
+ this.network = ctx.network;
169
+ this.caps = requireCapabilities(def);
170
+ networkConfig(def, ctx.network); // throws when the network is not offered
171
+ if (ctx.readOnly) {
172
+ this.client = new ArcusClient(ctx.network, null);
173
+ this.account = null;
174
+ this.masterCreds = null;
175
+ this.masterPk = null;
176
+ return;
177
+ }
178
+ this.masterCreds = ctx.credentials.owsWallet !== null
179
+ ? { wallet: ctx.credentials.owsWallet, passphrase: ctx.credentials.owsPassphrase }
180
+ : null;
181
+ this.masterPk = ctx.credentials.pk;
182
+ const hasMaster = this.masterCreds !== null || this.masterPk !== null;
183
+ // --no-agent ignores the agent key; an absent agent key is the same situation.
184
+ const meta = ctx.noAgent ? null : ctx.credentials.agent;
185
+ if (meta === null) {
186
+ if (!hasMaster) {
187
+ // §12.4 #7: no agent key and no master EVM key — nothing can sign.
188
+ throw new PerpError("NO_SIGNER_AVAILABLE", "No Arcus signer: no agent API key and no master EVM wallet / private key", {
189
+ remediation: "Spot swaps: pass --wallet <ows-wallet> (master EVM key). Perp trading: perp wallet agent approve arcus --master <ows-wallet>",
190
+ });
191
+ }
192
+ // Master-only (also --no-agent): spot swaps (master EVM signer) work; perp
193
+ // private calls throw AGENT_NOT_REGISTERED in requireAccount() (design §12.4 #7).
194
+ this.client = new ArcusClient(ctx.network, null);
195
+ this.account = null;
196
+ return;
197
+ }
198
+ if (!isArcusMeta(meta)) {
199
+ throw new PerpError("AGENT_NOT_REGISTERED", "No Arcus agent API key configured", {
200
+ remediation: "Run: perp wallet agent approve arcus --master <ows-wallet>",
201
+ });
202
+ }
203
+ if (meta.network !== ctx.network) {
204
+ throw new PerpError("AGENT_NOT_REGISTERED", `Arcus API key "${meta.agentName}" was registered on ${String(meta.network)}, not ${ctx.network}`, {
205
+ remediation: `Register a ${ctx.network} key: perp wallet agent approve arcus --master <ows-wallet>${ctx.network === "testnet" ? " -n testnet" : ""}`,
206
+ });
207
+ }
208
+ if (!(Date.parse(meta.validUntil) > Date.now())) {
209
+ throw new PerpError("AGENT_EXPIRED", `Arcus API key "${meta.agentName}" expired at ${meta.validUntil}`, {
210
+ remediation: "Re-register: perp wallet agent approve arcus --master <ows-wallet>",
211
+ });
212
+ }
213
+ if (!(Number.isInteger(meta.accountIndex) && meta.accountIndex >= 0 && meta.accountIndex <= 9)) {
214
+ throw new PerpError("INVALID_PARAMS", `Arcus agent "${meta.agentName}" is scoped to accountIndex ${meta.accountIndex}; trading needs a single subaccount 0–9`, {
215
+ remediation: "Approve a key for one subaccount: perp wallet agent approve arcus --account-index <0-9>",
216
+ });
217
+ }
218
+ // Agent wallets are created with an empty passphrase (Pacifica agent pattern).
219
+ const agentSigner = OwsSolanaSigner.create(meta.agentWalletName, "");
220
+ const pubHex = ed25519PubHexFromBase58(agentSigner.getPublicKeyBase58());
221
+ if (pubHex !== meta.apiKeyHex.toLowerCase()) {
222
+ throw new PerpError("KEY_NOT_FOUND", `OWS wallet "${meta.agentWalletName}" does not hold Arcus API key ${meta.apiKeyHex}`, {
223
+ remediation: "Re-register: perp wallet agent approve arcus --master <ows-wallet>",
224
+ });
225
+ }
226
+ const sign = m => agentSigner.signMessage(m);
227
+ this.client = new ArcusClient(ctx.network, { apiKeyHex: pubHex, sign, ownerAddress: meta.ownerAddress });
228
+ this.account = { owner: meta.ownerAddress, sub: meta.accountIndex };
229
+ }
230
+ // ── Internals ──
231
+ /**
232
+ * Single guard for every perp private call (orders / cancel / amend /
233
+ * leverage / private reads / withdraw): they all need the Ed25519 agent key.
234
+ * Master-only adapters (spot swaps) and read-only adapters land here.
235
+ */
236
+ requireAccount() {
237
+ if (!this.account) {
238
+ throw new PerpError("AGENT_NOT_REGISTERED", "Arcus perp calls need an agent API key, and none is registered", {
239
+ exchange: "arcus", remediation: "perp wallet agent approve arcus",
240
+ });
241
+ }
242
+ return this.account;
243
+ }
244
+ accountQuery(extra = {}) {
245
+ const a = this.requireAccount();
246
+ return { address: a.owner, accountIndex: a.sub, ...extra };
247
+ }
248
+ async loadMarkets(fresh) {
249
+ if (!fresh && this.markets && Date.now() - this.markets.at < MARKETS_TTL_MS)
250
+ return this.markets;
251
+ const raw = await fetchArcusRawMarkets(this.client);
252
+ const bySymbol = new Map();
253
+ const byId = new Map();
254
+ for (const m of raw) {
255
+ bySymbol.set(m.baseAsset.toUpperCase(), m);
256
+ bySymbol.set(m.marketDisplayName.toUpperCase(), m);
257
+ byId.set(m.marketId, m);
258
+ }
259
+ this.markets = { at: Date.now(), bySymbol, byId };
260
+ return this.markets;
261
+ }
262
+ async market(symbol, fresh = false) {
263
+ const idx = await this.loadMarkets(fresh);
264
+ const m = idx.bySymbol.get(symbol.toUpperCase());
265
+ if (!m) {
266
+ throw new PerpError("SYMBOL_NOT_FOUND", `Arcus: unknown market "${symbol}"`, {
267
+ remediation: "List markets with: perp market list -e arcus",
268
+ });
269
+ }
270
+ return m;
271
+ }
272
+ async symbolOf(marketId) {
273
+ let m = (await this.loadMarkets(false)).byId.get(marketId);
274
+ if (!m)
275
+ m = (await this.loadMarkets(true)).byId.get(marketId);
276
+ if (!m)
277
+ throw new PerpError("EXCHANGE_ERROR", `Arcus: unknown marketId ${marketId}`);
278
+ return m.baseAsset;
279
+ }
280
+ /** Protection price: slippage from `ref`, aligned toward crossing on the tier tick, ≤ 10 % from `ref`. */
281
+ protectionPrice(m, ref, side, bps, what) {
282
+ const raw = slippagePrice(ref, side, bps);
283
+ const aligned = alignProtectionPrice(raw, tickForPrice(m, raw), side);
284
+ const dev = Math.abs(Number(aligned) - Number(ref)) / Number(ref);
285
+ if (dev > ARCUS_MAX_PROTECTION_DEVIATION) {
286
+ throw new PerpError("INVALID_PARAMS", `Arcus: protection price ${aligned} is ${(dev * 100).toFixed(2)}% from ${what} ${ref} (max 10%)`, {
287
+ remediation: "Lower --slippage-bps (Arcus rejects protection prices more than 10% from mark)",
288
+ });
289
+ }
290
+ return aligned;
291
+ }
292
+ /** Limit price must sit on its tier's tick; the signed integer always uses the top-level tickSize. */
293
+ limitTicks(m, price) {
294
+ requirePositive(price, "price");
295
+ toUnitsExact(price, tickForPrice(m, price), "price");
296
+ return toUnitsExact(price, m.tickSize, "price");
297
+ }
298
+ leg(m, p) {
299
+ const size = floorSize(p.size, m.stepSize);
300
+ const priceTicks = this.limitTicks(m, p.price);
301
+ // docs.arcus.xyz GET /markets → minOrderNotional: "Flat minimum order notional (`quantity` ×
302
+ // `price`, decimal USD) on position-opening orders. Reduce-only orders (including TPSLs) are
303
+ // exempt." Pre-checked from the market's own value (null = the venue published none).
304
+ if (!p.reduceOnly && m.minOrderNotional !== null) {
305
+ const notional = mulDecimal(size, p.price);
306
+ if (subDecimal(notional, m.minOrderNotional).startsWith("-")) {
307
+ throw new PerpError("SIZE_TOO_SMALL", `Arcus ${m.marketDisplayName}: order notional ${notional} (${size} × ${p.price}) is below the market minimum ${m.minOrderNotional} USD; nothing was sent`, {
308
+ exchange: "arcus", remediation: `Raise the size to at least ${m.minOrderNotional} USD notional (reduce-only orders are exempt)`,
309
+ });
310
+ }
311
+ }
312
+ return { ...p, size, quantums: toUnitsExact(size, m.stepSize, "size"), priceTicks };
313
+ }
314
+ async signLeg(m, leg, ctNs, g) {
315
+ const a = this.requireAccount();
316
+ const auth = this.client.requireAuth();
317
+ assertArcusClientId(leg.clientId);
318
+ const payload = placePayload({
319
+ address: a.owner, accountIndex: a.sub, clientId: leg.clientId, ctNs, goodTilNs: g * 1000n,
320
+ marketId: m.marketId, priceTicks: leg.priceTicks, quantums: leg.quantums, reduceOnly: leg.reduceOnly,
321
+ side: leg.side, tif: leg.tif,
322
+ }, leg.tpsl !== null);
323
+ const signature = await signUtf8Hex(auth.sign, payload);
324
+ const body = {
325
+ address: a.owner,
326
+ accountIndex: a.sub,
327
+ marketId: m.marketId,
328
+ orderSide: leg.side === "buy" ? "BUY" : "SELL",
329
+ orderType: leg.orderType,
330
+ quantity: leg.size,
331
+ price: leg.price,
332
+ timeInForce: leg.tif,
333
+ goodTilTime: g.toString(),
334
+ reduceOnly: leg.reduceOnly,
335
+ clientId: leg.clientId,
336
+ clientTime: ctNs.toString(),
337
+ timestamp: ctNs,
338
+ ...(leg.tpsl ? { tpslType: leg.tpsl.type, stopPrice: leg.tpsl.stopPrice } : {}),
339
+ };
340
+ return { body, signature };
341
+ }
342
+ /**
343
+ * Placement / modify acknowledgement → OrderResult.
344
+ *
345
+ * filledSize (M9): on the 202 path the body is an ACK "carrying no terminal
346
+ * state" and the enrichment fields (filledSize, remainingSize) are omitted
347
+ * (openapi OrderResponse / BatchOrderItemResponse / ModifyOrderResponse). An
348
+ * ACK has not been matched yet, so it has no fills by definition: "0" is used
349
+ * ONLY for status "accepted" (finality pending). On the 200 path the fill is
350
+ * read from filledSize, else derived as quantity − remainingSize (the docs
351
+ * define filledSize = originalSize − remainingSize); otherwise it is unknown
352
+ * and EXCHANGE_ERROR is thrown (the order exists — do not retry blindly).
353
+ */
354
+ resultFromPlace(symbol, r, fallbackClientId, raw, exec) {
355
+ if (r.status === "ERROR") {
356
+ throw arcusRejectionError(r.rejectionReason, `Arcus order rejected: ${r.error ?? r.rejectionReason ?? "ERROR"}`, { venueRaw: raw });
357
+ }
358
+ const status = mapStatus(r.status);
359
+ let filledSize;
360
+ if (r.filledSize !== undefined) {
361
+ filledSize = r.filledSize;
362
+ }
363
+ else if (r.quantity !== undefined && r.remainingSize !== undefined) {
364
+ filledSize = subDecimal(r.quantity, r.remainingSize);
365
+ }
366
+ else if (status === "accepted") {
367
+ filledSize = "0"; // ACK: not yet matched → no fills (see above)
368
+ }
369
+ else {
370
+ throw new PerpError("EXCHANGE_ERROR", `Arcus returned status ${r.status} for order ${r.orderId} without a fill size`, {
371
+ venueRaw: raw,
372
+ remediation: `The order exists; do not resubmit. Check it: perp trade status ${r.orderId} --symbol ${symbol} -e arcus`,
373
+ });
374
+ }
375
+ const clientOrderId = r.clientId ?? fallbackClientId;
376
+ const orderId = r.orderId || null;
377
+ if (orderId === null && clientOrderId === null) {
378
+ throw new PerpError("EXCHANGE_ERROR", "Arcus acknowledgement carries neither orderId nor clientId", { venueRaw: raw });
379
+ }
380
+ return {
381
+ venue: "arcus",
382
+ symbol,
383
+ status,
384
+ finality: finalityOf(status),
385
+ orderId,
386
+ clientOrderId,
387
+ filledSize,
388
+ avgPrice: null,
389
+ execution: exec,
390
+ venueRaw: raw,
391
+ };
392
+ }
393
+ /** Order shape: filledSize is optional and documented as originalSize − remainingSize (both required). */
394
+ filledOf(o) {
395
+ return o.filledSize ?? subDecimal(o.originalSize, o.remainingSize);
396
+ }
397
+ orderResultFromRaw(symbol, o) {
398
+ const status = mapStatus(o.status);
399
+ return {
400
+ venue: "arcus",
401
+ symbol,
402
+ status,
403
+ finality: finalityOf(status),
404
+ orderId: o.orderId,
405
+ clientOrderId: o.clientId ?? null,
406
+ filledSize: this.filledOf(o),
407
+ avgPrice: o.avgFillPrice ?? null,
408
+ execution: readExecution(o),
409
+ venueRaw: o,
410
+ };
411
+ }
412
+ /** TP/SL classification of a reduce-only trigger relative to the current mark. */
413
+ tpslType(side, trigger, mark) {
414
+ const t = Number(trigger);
415
+ const mk = Number(mark);
416
+ if (t === mk) {
417
+ throw new PerpError("INVALID_PARAMS", `Arcus: trigger ${trigger} equals mark ${mark}; cannot classify as TP or SL`);
418
+ }
419
+ // Closing a long (sell): below mark = stop loss. Closing a short (buy): above mark = stop loss.
420
+ return side === "sell" ? (t < mk ? "STOP_LOSS" : "TAKE_PROFIT") : (t > mk ? "STOP_LOSS" : "TAKE_PROFIT");
421
+ }
422
+ async findOrder(m, ref) {
423
+ if (ref.kind === "orderId") {
424
+ try {
425
+ return await this.client.get(`/v1/order/${encodeURIComponent(ref.value)}`, "order", this.accountQuery());
426
+ }
427
+ catch (e) {
428
+ // GET /v1/order/{id}: 404 = "Order not found" (openapi), whatever the message text.
429
+ if (e instanceof PerpError && e.structured.details?.status === 404 && e.structured.code === "EXCHANGE_ERROR") {
430
+ throw new PerpError("ORDER_NOT_FOUND", e.message, e.structured.details);
431
+ }
432
+ throw e;
433
+ }
434
+ }
435
+ const open = await this.client.get("/v1/openOrders", "openOrders", this.accountQuery({ market: m.marketDisplayName }));
436
+ const hit = open.orders.find(o => o.clientId === ref.value);
437
+ if (hit)
438
+ return hit;
439
+ const hist = await this.client.get("/v1/orders", "orders", this.accountQuery({ market: m.marketDisplayName, limit: CLIENT_ID_HISTORY_SCAN }));
440
+ const past = hist.orders.find(o => o.clientId === ref.value);
441
+ if (past)
442
+ return past;
443
+ throw new PerpError("ORDER_NOT_FOUND", `Arcus: no order with clientId "${ref.value}" on ${m.marketDisplayName}`, {
444
+ remediation: `Only the newest ${CLIENT_ID_HISTORY_SCAN} orders of the market are scanned; query by server order id instead`,
445
+ });
446
+ }
447
+ // ── TradingV2 ──
448
+ async getVenueMarkets() {
449
+ const raw = await fetchArcusRawMarkets(this.client);
450
+ return raw.map(toArcusVenueMarket);
451
+ }
452
+ async placeOrder(input) {
453
+ // §13.3: GTC is the uniform default for a limit order without a TIF.
454
+ const tifRequested = input.tif;
455
+ const req = input.type === "limit" && input.tif === null ? { ...input, tif: "GTC" } : input;
456
+ validateOrderRequest(this.venue, this.caps, req);
457
+ this.requireAccount();
458
+ const clientId = req.clientOrderId;
459
+ if (clientId === null)
460
+ throw new PerpError("INVALID_PARAMS", "unreachable: validateOrderRequest enforces clientOrderId for async venues");
461
+ assertArcusClientId(clientId);
462
+ const isStop = req.type === "stopMarket" || req.type === "stopLimit";
463
+ const needsMark = req.type === "market" || isStop || req.tpsl !== null;
464
+ const m = await this.market(req.symbol, needsMark);
465
+ if (m.status !== "ONLINE") {
466
+ throw new PerpError("INVALID_PARAMS", `Arcus market ${m.marketDisplayName} is ${m.status}`, { remediation: "Market is not tradable right now" });
467
+ }
468
+ const mark = m.markPrice;
469
+ if (needsMark && (mark === undefined || !(Number(mark) > 0))) {
470
+ throw new PerpError("PRICE_STALE", `Arcus ${m.marketDisplayName}: no mark price`);
471
+ }
472
+ // One goodTil for every leg of this placement; resting legs report it as expiresAt.
473
+ const gMs = goodTilAtMs();
474
+ const g = BigInt(gMs) * 1000n;
475
+ // Stop orders: a single reduce-only TPSL leg in a partialTpsl group.
476
+ if (isStop) {
477
+ const trigger = req.triggerPrice; // validated non-null
478
+ toUnitsExact(trigger, tickForPrice(m, trigger), "trigger price");
479
+ const type = this.tpslType(req.side, trigger, mark);
480
+ let leg;
481
+ let exec;
482
+ if (req.type === "stopMarket") {
483
+ if (req.slippage === null) {
484
+ throw new PerpError("INVALID_PARAMS", "Arcus stop-market legs are protection-priced; the order is missing its resolved slippage bound");
485
+ }
486
+ const px = this.protectionPrice(m, trigger, req.side, req.slippage.bps, "trigger price");
487
+ leg = this.leg(m, { side: req.side, size: req.size, price: px, tif: "IOC", orderType: "MARKET", reduceOnly: true, clientId, tpsl: { type, stopPrice: trigger } });
488
+ exec = execution("trigger", {
489
+ slippageBps: appliedBps(px, trigger), slippageSource: req.slippage.source, protectionPrice: px, referencePrice: trigger, tifSent: "IOC",
490
+ notes: [`${type} leg (partialTpsl); protection price = trigger ± ${req.slippage.bps} bps — may not fill if the price gaps past it`],
491
+ });
492
+ }
493
+ else {
494
+ const tif = toArcusTif(this.venue, req.tif);
495
+ if (tif !== "GTT") {
496
+ throw unsupported(this.venue.displayName, "timeInForce", `stop-limit (TPSL LIMIT) orders rest until triggered (GTC); got ${req.tif}`, "Pass --tif GTC or omit it");
497
+ }
498
+ leg = this.leg(m, { side: req.side, size: req.size, price: req.price, tif: "GTT", orderType: "LIMIT", reduceOnly: true, clientId, tpsl: { type, stopPrice: trigger } });
499
+ exec = execution("triggerLimit", {
500
+ referencePrice: trigger, tifRequested: req.tif, tifSent: "GTT", expiresAt: gMs,
501
+ notes: [`${type} leg (partialTpsl)`, ...(req.tif === "GTC" ? [`GTC sent as GTT; expires ${new Date(gMs).toISOString()}`] : [])],
502
+ });
503
+ }
504
+ return this.withFloorNote(await this.sendBatch(req.symbol, m, "partialTpsl", [leg], g, exec), req.size, leg.size);
505
+ }
506
+ let entry;
507
+ let exec;
508
+ if (req.type === "market") {
509
+ const slippage = req.slippage; // validated non-null for market
510
+ const px = this.protectionPrice(m, mark, req.side, slippage.bps, "mark");
511
+ entry = this.leg(m, { side: req.side, size: req.size, price: px, tif: "IOC", orderType: "LIMIT", reduceOnly: req.reduceOnly, clientId, tpsl: null });
512
+ exec = execution("protectedIoc", {
513
+ slippageBps: appliedBps(px, mark), slippageSource: slippage.source, protectionPrice: px, referencePrice: mark, tifSent: "IOC",
514
+ notes: [`resolved bound ${slippage.bps} bps (${slippage.source}); protection price aligned to the tier tick on the mark price`],
515
+ });
516
+ }
517
+ else {
518
+ const tif = toArcusTif(this.venue, req.tif);
519
+ entry = this.leg(m, { side: req.side, size: req.size, price: req.price, tif, orderType: "LIMIT", reduceOnly: req.reduceOnly, clientId, tpsl: null });
520
+ const resting = tif === "GTT" || tif === "ALO";
521
+ exec = execution("limit", {
522
+ tifRequested, tifSent: tif, expiresAt: resting ? gMs : null,
523
+ notes: [
524
+ ...(tifRequested === null ? ["no time-in-force given; GTC (uniform default)"] : []),
525
+ ...(req.tif === "GTC" ? [`GTC sent as GTT; expires ${new Date(gMs).toISOString()} (venue horizon)`] : []),
526
+ ],
527
+ });
528
+ }
529
+ if (req.tpsl === null) {
530
+ const res = await this.client.postScheme1("/v1/placeOrder", "placeOrder", async (ct) => {
531
+ const { body, signature } = await this.signLeg(m, entry, ct, g);
532
+ return { bodyJson: canonicalJson(body), signatureHex: signature };
533
+ });
534
+ return this.withFloorNote(this.resultFromPlace(req.symbol, res.data, clientId, res.data, exec), req.size, entry.size);
535
+ }
536
+ if (clientId.length > ARCUS_TPSL_CLIENT_ID_MAX) {
537
+ throw new PerpError("INVALID_PARAMS", `Arcus clientId "${clientId}" is ${clientId.length} chars; with TP/SL legs ("-tp"/"-sl" suffix) it must be at most ${ARCUS_TPSL_CLIENT_ID_MAX}`);
538
+ }
539
+ // Entry + bound TP/SL children (entryTpsl). Children are reduce-only MARKET
540
+ // legs whose protection price needs an explicit slippage bound.
541
+ if (req.slippage === null) {
542
+ throw new PerpError("INVALID_PARAMS", "Arcus TP/SL legs execute as MARKET orders; the order is missing its resolved slippage bound");
543
+ }
544
+ const tpslBps = req.slippage.bps;
545
+ const exitSide = req.side === "buy" ? "sell" : "buy";
546
+ const legs = [entry];
547
+ for (const [kind, trig] of [["TAKE_PROFIT", req.tpsl.tp], ["STOP_LOSS", req.tpsl.sl]]) {
548
+ if (trig === null)
549
+ continue;
550
+ toUnitsExact(trig, tickForPrice(m, trig), "trigger price");
551
+ const px = this.protectionPrice(m, trig, exitSide, tpslBps, "trigger price");
552
+ legs.push(this.leg(m, {
553
+ side: exitSide, size: entry.size, price: px, tif: "IOC", orderType: "MARKET", reduceOnly: true,
554
+ clientId: `${clientId}-${kind === "TAKE_PROFIT" ? "tp" : "sl"}`, tpsl: { type: kind, stopPrice: trig },
555
+ }));
556
+ exec.notes.push(`${kind} leg (entryTpsl): trigger ${trig}, protection price ${px} (${tpslBps} bps, ${req.slippage.source})`);
557
+ }
558
+ return this.withFloorNote(await this.sendBatch(req.symbol, m, "entryTpsl", legs, g, exec), req.size, entry.size);
559
+ }
560
+ /** Size flooring is surfaced, never silent (L2): wrap venueRaw with requested/placed sizes. */
561
+ withFloorNote(r, requested, placed) {
562
+ if (subDecimal(requested, placed) === "0")
563
+ return r;
564
+ return { ...r, venueRaw: { requestedSize: requested, placedSize: placed, response: r.venueRaw } };
565
+ }
566
+ /** batchPlaceOrders; returns the first leg's result, throws if any leg failed validation. */
567
+ async sendBatch(symbol, m, grouping, legs, g, exec) {
568
+ if (legs.length > (this.caps.batchPlaceMax ?? 0))
569
+ throw new PerpError("INVALID_PARAMS", "Arcus batch too large");
570
+ // X-Signature must be present; the batch is verified per element.
571
+ const res = await this.client.postScheme1("/v1/batchPlaceOrders", "batchPlaceOrders", async (ct) => {
572
+ const signed = [];
573
+ for (const l of legs)
574
+ signed.push(await this.signLeg(m, l, ct, g));
575
+ const body = { grouping, orders: signed.map(s => ({ ...s.body, signature: s.signature })) };
576
+ return { bodyJson: canonicalJson(body), signatureHex: signed[0].signature };
577
+ });
578
+ const rows = res.data.responses;
579
+ if (!Array.isArray(rows) || rows.length !== legs.length) {
580
+ throw new PerpError("EXCHANGE_ERROR", "Arcus batchPlaceOrders: unexpected response shape", { venueRaw: res.data });
581
+ }
582
+ const failed = rows.map((r, i) => ({ r, leg: legs[i] })).filter(x => x.r.status === "ERROR");
583
+ if (failed.length > 0) {
584
+ const summary = failed.map(f => `${f.leg.clientId}: ${f.r.error ?? f.r.rejectionReason}`).join("; ");
585
+ const accepted = rows.map((r, i) => ({ r, leg: legs[i] })).filter(x => x.r.status !== "ERROR");
586
+ if (accepted.length === 0) {
587
+ throw arcusRejectionError(failed[0].r.rejectionReason, `Arcus ${grouping}: ${summary}`, { venueRaw: res.data });
588
+ }
589
+ throw new PerpError("ORDER_PARTIAL", `Arcus ${grouping}: some legs failed (${summary}) while others were accepted`, {
590
+ accepted: accepted.map(x => ({ orderId: x.r.orderId || null, clientId: x.leg.clientId, status: x.r.status })),
591
+ venueRaw: res.data,
592
+ remediation: `Do not retry; check open orders and positions: perp account orders -e arcus && perp account positions -e arcus`,
593
+ });
594
+ }
595
+ return this.resultFromPlace(symbol, rows[0], legs[0].clientId, res.data, exec);
596
+ }
597
+ /**
598
+ * modifyOrder (docs modify-order / authentication).
599
+ *
600
+ * Size: the CLI's `req.size` is the new OPEN (unfilled) size (design §13.4), while
601
+ * Arcus modifies take the order's new TOTAL size, filled quantity included
602
+ * (OrderRejectionReason MODIFY_SIZE_ALREADY_FILLED: "the order's new TOTAL size,
603
+ * filled quantity included"). So body `quantity` and signed `q` are
604
+ * filledSize (as read just before) + new open size. A new open size ≤ 0 is a
605
+ * cancel, not a modify. A fill that races the modify shrinks the resting
606
+ * remainder; if fills reach the new total the venue cancels the order and rejects
607
+ * with MODIFY_SIZE_ALREADY_FILLED — reported as the order's read-back (cancelled) state.
608
+ *
609
+ * Identity: the body carries `orderId` only (exactly one of orderId / clientId);
610
+ * the signed payload follows ARCUS_MODIFY_SIGNED_IDENTITY (UNVERIFIED, signing.ts).
611
+ * g/r/s/t echo the resting order; `clientTime` = signed `ct` = X-Timestamp.
612
+ */
613
+ async amendOrder(req) {
614
+ const a = this.requireAccount();
615
+ const auth = this.client.requireAuth();
616
+ const m = await this.market(req.symbol);
617
+ const o = await this.findOrder(m, req.ref);
618
+ if (o.marketId !== m.marketId) {
619
+ throw new PerpError("INVALID_PARAMS", `Arcus order ${o.orderId} is on ${o.marketDisplayName}, not ${m.marketDisplayName}`);
620
+ }
621
+ if (o.tpslType) {
622
+ throw unsupported(this.venue.displayName, "amend", "TP/SL orders cannot be modified (MODIFY_TPSL_NOT_SUPPORTED)", "Cancel the TP/SL order and place a new one");
623
+ }
624
+ const newOpen = subDecimal(req.size, "0"); // validates the decimal
625
+ if (newOpen === "0" || newOpen.startsWith("-")) {
626
+ const c = await this.cancelByRef(req.symbol, { kind: "orderId", value: o.orderId });
627
+ if (c.failed.length > 0) {
628
+ throw new PerpError("EXCHANGE_ERROR", `Arcus: amend to size ${req.size} = cancel, and the cancel failed: ${c.failed[0].error}`, { venueRaw: c.venueRaw });
629
+ }
630
+ const st = c.venueRaw.status;
631
+ return {
632
+ venue: "arcus", symbol: req.symbol, status: st === "CANCELED" ? "cancelled" : "accepted",
633
+ finality: st === "CANCELED" ? "final" : "pending", orderId: o.orderId, clientOrderId: o.clientId ?? null,
634
+ filledSize: this.filledOf(o), avgPrice: o.avgFillPrice ?? null,
635
+ execution: execution("cancel", { notes: [`new open size ${req.size} ≤ 0 → order cancelled instead of modified`] }),
636
+ venueRaw: c.venueRaw,
637
+ };
638
+ }
639
+ if (o.goodTilTime === undefined) {
640
+ throw new PerpError("EXCHANGE_ERROR", `Arcus order ${o.orderId} has no goodTilTime to echo`, { venueRaw: o });
641
+ }
642
+ const openSize = floorSize(req.size, m.stepSize);
643
+ const filled = this.filledOf(o);
644
+ const totalQuantums = toUnitsExact(openSize, m.stepSize, "size") + toUnitsExact(filled, m.stepSize, "filled size");
645
+ const total = unitsToString(totalQuantums, m.stepSize);
646
+ const side = sideLower(o.side);
647
+ const tif = tifFromRead(o.timeInForce);
648
+ if (req.tif !== null && toArcusTif(this.venue, req.tif) !== tif) {
649
+ throw unsupported(this.venue.displayName, "amendTimeInForce", `modify keeps the resting order's time-in-force (${tif}); got ${req.tif}`, "Omit --tif, or cancel and place a new order with the other time-in-force");
650
+ }
651
+ // The modify re-signs reduce-only: a read-back without a boolean reduceOnly would silently
652
+ // turn a reduce-only order into one that can open / flip a position (r=0). Refuse.
653
+ if (typeof o.reduceOnly !== "boolean") {
654
+ throw new PerpError("EXCHANGE_ERROR", `Arcus order ${o.orderId} read-back has no reduceOnly flag (got ${JSON.stringify(o.reduceOnly ?? null)}); refusing to modify`, {
655
+ exchange: "arcus", venueRaw: o,
656
+ remediation: `Cancel it (perp trade cancel ${req.symbol} ${o.orderId} -e arcus) and place a new order`,
657
+ });
658
+ }
659
+ const reduceOnly = o.reduceOnly;
660
+ const gUs = BigInt(o.goodTilTime);
661
+ const priceTicks = this.limitTicks(m, req.price);
662
+ const resting = tif === "GTT" || tif === "ALO";
663
+ const exec = execution("nativeAmend", {
664
+ tifRequested: req.tif, tifSent: tif, expiresAt: resting ? goodTilMs(o.goodTilTime) : null,
665
+ notes: [
666
+ "modifyOrder keeps the resting order's time-in-force, side, reduce-only and goodTil",
667
+ `sent total size ${total} = filled ${filled} + new open ${openSize}`,
668
+ ],
669
+ });
670
+ let res;
671
+ try {
672
+ res = await this.client.postScheme1("/v1/modifyOrder", "modifyOrder", async (ct) => {
673
+ const payload = modifyPayload({
674
+ address: a.owner, accountIndex: a.sub, clientId: o.clientId ?? null, ctNs: ct, goodTilNs: gUs * 1000n,
675
+ marketId: m.marketId, priceTicks, quantums: totalQuantums, reduceOnly, side, tif,
676
+ }, o.orderId);
677
+ const body = {
678
+ address: a.owner, accountIndex: a.sub, marketId: m.marketId, orderId: o.orderId,
679
+ goodTilTime: o.goodTilTime, quantity: total, price: req.price, side: o.side, timeInForce: tif,
680
+ reduceOnly, clientTime: ct.toString(),
681
+ };
682
+ return { bodyJson: canonicalJson(body), signatureHex: await signUtf8Hex(auth.sign, payload) };
683
+ });
684
+ }
685
+ catch (e) {
686
+ if (e instanceof PerpError && e.structured.details?.rejectionReason === "MODIFY_SIZE_ALREADY_FILLED") {
687
+ return this.modifyAlreadyFilled(req.symbol, o.orderId, exec);
688
+ }
689
+ throw e;
690
+ }
691
+ if (res.data.status === "ERROR" && res.data.rejectionReason === "MODIFY_SIZE_ALREADY_FILLED") {
692
+ return this.modifyAlreadyFilled(req.symbol, o.orderId, exec);
693
+ }
694
+ const acked = this.resultFromPlace(req.symbol, res.data, o.clientId ?? null, res.data, exec);
695
+ return this.withFloorNote(await this.confirmModify(m, acked, o.orderId, req.price, total), req.size, openSize);
696
+ }
697
+ /**
698
+ * A modify ack (ACK / OPEN) is not the edited order's state: read the order back
699
+ * (GET /v1/order/{id}, bounded 5 × 1 s). Final when it is terminal (filled / cancelled /
700
+ * rejected), or resting with the new price and total size; otherwise the ack stays pending.
701
+ */
702
+ async confirmModify(m, acked, orderId, price, total) {
703
+ if (acked.finality === "final")
704
+ return acked;
705
+ let lastErr = null;
706
+ for (let attempt = 0; attempt < 5; attempt++) {
707
+ await new Promise(r => setTimeout(r, 1_000));
708
+ let o;
709
+ try {
710
+ o = await this.findOrder(m, { kind: "orderId", value: orderId });
711
+ }
712
+ catch (e) {
713
+ lastErr = e instanceof Error ? e.message : String(e);
714
+ continue;
715
+ }
716
+ const read = this.orderResultFromRaw(acked.symbol, o);
717
+ const done = read.finality === "final"
718
+ || ((read.status === "open" || read.status === "partiallyFilled") && Number(o.price) === Number(price) && Number(o.originalSize) === Number(total));
719
+ if (done) {
720
+ return {
721
+ ...read, finality: "final", execution: acked.execution,
722
+ venueRaw: { ack: acked.venueRaw, order: o },
723
+ };
724
+ }
725
+ }
726
+ acked.execution.notes.push(`the edited order was not confirmed at price ${price} / total ${total}${lastErr !== null ? ` (last read error: ${lastErr})` : ""}`);
727
+ return acked;
728
+ }
729
+ /**
730
+ * MODIFY_SIZE_ALREADY_FILLED: fills reached the new total, so the venue cancelled the
731
+ * original order. Report the order's read-back state (cancelled, with its real fills).
732
+ */
733
+ async modifyAlreadyFilled(symbol, orderId, exec) {
734
+ const m = await this.market(symbol);
735
+ const after = this.orderResultFromRaw(symbol, await this.findOrder(m, { kind: "orderId", value: orderId }));
736
+ return {
737
+ ...after,
738
+ execution: { ...exec, notes: [...exec.notes, "MODIFY_SIZE_ALREADY_FILLED: fills already reached the new size — the original order was cancelled"] },
739
+ };
740
+ }
741
+ async cancelByRef(symbol, ref) {
742
+ const a = this.requireAccount();
743
+ const auth = this.client.requireAuth();
744
+ const m = await this.market(symbol);
745
+ const res = await this.client.postScheme1("/v1/cancelOrder", "cancelOrder", async (ct) => {
746
+ const signature = await signUtf8Hex(auth.sign, cancelPayload({ address: a.owner, accountIndex: a.sub, ctNs: ct, marketId: m.marketId, ref }));
747
+ const body = {
748
+ address: a.owner, accountIndex: a.sub, marketId: m.marketId, timestamp: ct,
749
+ ...(ref.kind === "orderId" ? { kind: "orderId", orderId: ref.value } : { kind: "clientId", clientId: ref.value }),
750
+ };
751
+ return { bodyJson: canonicalJson(body), signatureHex: signature };
752
+ });
753
+ const st = res.data.status;
754
+ // CANCEL_ACKNOWLEDGED (202) = forwarded to the engine; CANCELED (200) = definitive.
755
+ if (st === "CANCEL_ACKNOWLEDGED" || st === "CANCELED") {
756
+ return { venue: "arcus", cancelled: [ref], failed: [], venueRaw: res.data };
757
+ }
758
+ return { venue: "arcus", cancelled: [], failed: [{ ref, error: res.data.rejectionReason ?? st }], venueRaw: res.data };
759
+ }
760
+ /**
761
+ * Every open order (GET /v1/openOrders pages hold ≤ 1000 rows, newest first; the next
762
+ * page is `to` = the oldest `createdAt` seen, inclusive → dedupe by orderId).
763
+ */
764
+ async allOpenOrders(market) {
765
+ const seen = new Map();
766
+ let to = null;
767
+ for (;;) {
768
+ const page = await this.client.get("/v1/openOrders", "openOrders", this.accountQuery({ market, limit: OPEN_ORDERS_PAGE, to }));
769
+ let added = 0;
770
+ for (const o of page.orders) {
771
+ if (!seen.has(o.orderId)) {
772
+ seen.set(o.orderId, o);
773
+ added++;
774
+ }
775
+ }
776
+ if (page.orders.length < OPEN_ORDERS_PAGE)
777
+ return [...seen.values()];
778
+ if (added === 0) {
779
+ throw new PerpError("EXCHANGE_ERROR", `Arcus openOrders: a full page of ${OPEN_ORDERS_PAGE} rows added no new order — cannot page further`, {
780
+ remediation: "Cancel per market (perp trade cancel-all <symbol> -e arcus) and retry",
781
+ });
782
+ }
783
+ const oldest = Math.min(...page.orders.map(o => {
784
+ if (o.createdAt === undefined) {
785
+ throw new PerpError("EXCHANGE_ERROR", "Arcus openOrders row without createdAt — cannot page past 1000 open orders", { venueRaw: o });
786
+ }
787
+ return o.createdAt;
788
+ }));
789
+ to = oldest;
790
+ }
791
+ }
792
+ async cancelAllV2(symbol) {
793
+ const a = this.requireAccount();
794
+ const m = symbol !== null ? await this.market(symbol) : null;
795
+ const open = { orders: await this.allOpenOrders(m ? m.marketDisplayName : null) };
796
+ const body = { accountIndex: a.sub, address: a.owner, ...(m ? { marketId: m.marketId } : {}) };
797
+ const res = await this.client.postScheme2("cancelAllOrders", body);
798
+ if (res.data.status !== "CANCEL_ALL_ACKNOWLEDGED") {
799
+ throw new PerpError("EXCHANGE_ERROR", `Arcus cancelAllOrders: unexpected status ${res.data.status}`, { venueRaw: res.data });
800
+ }
801
+ // Acknowledged: the orders open just before the request are being cancelled by the engine.
802
+ return {
803
+ venue: "arcus",
804
+ cancelled: open.orders.map(o => ({ kind: "orderId", value: o.orderId })),
805
+ failed: [],
806
+ venueRaw: res.data,
807
+ };
808
+ }
809
+ async applyLeverage(symbol, leverage, marginMode) {
810
+ const a = this.requireAccount();
811
+ if (!Number.isInteger(leverage) || leverage < 1) {
812
+ throw new PerpError("INVALID_PARAMS", `Arcus leverage must be a positive integer (got ${leverage})`);
813
+ }
814
+ const m = await this.market(symbol);
815
+ const body = {
816
+ accountIndex: a.sub, address: a.owner, leverage, marketId: m.marketId,
817
+ ...(marginMode !== null ? { isolated: marginMode === "isolated" } : {}),
818
+ };
819
+ const res = await this.client.postScheme2("setLeverage", body);
820
+ if (res.data.status === "REJECTED") {
821
+ throw arcusLeverageRejectError(res.data.rejectReason, `Arcus setLeverage rejected: ${res.data.rejectReason ?? "REJECTED"}`, { venueRaw: res.data });
822
+ }
823
+ let mode;
824
+ if (res.data.marginMode) {
825
+ mode = res.data.marginMode === "ISOLATED" ? "isolated" : "cross";
826
+ }
827
+ else {
828
+ const lev = await this.client.get("/v1/leverages", "leverages", this.accountQuery());
829
+ const entry = lev.leverages.find(l => l.marketId === m.marketId);
830
+ if (!entry)
831
+ throw new PerpError("EXCHANGE_ERROR", `Arcus /v1/leverages: no entry for ${m.marketDisplayName}`);
832
+ mode = entry.marginMode === "ISOLATED" ? "isolated" : "cross";
833
+ }
834
+ return { venue: "arcus", symbol, leverage: res.data.leverage, marginMode: mode, venueRaw: res.data };
835
+ }
836
+ /**
837
+ * Per-market leverage / margin mode: GET /v1/leverages (docs.arcus.xyz openapi getLeverages:
838
+ * "Returns the effective leverage and margin mode (CROSS/ISOLATED) for `(address,
839
+ * accountIndex)` across every market … Each entry reflects the user's leverage override (set
840
+ * via `POST /v1/setLeverage`) when one exists, otherwise the market default"; LeverageEntry
841
+ * requires marketId, marketDisplayName, leverage (integer ≥ 1), isolated, marginMode).
842
+ */
843
+ async getMarketSettings() {
844
+ const res = await this.client.get("/v1/leverages", "leverages", this.accountQuery());
845
+ if (!Array.isArray(res.leverages))
846
+ throw new PerpError("EXCHANGE_ERROR", "Arcus /v1/leverages: no leverages list");
847
+ const out = [];
848
+ for (const l of res.leverages) {
849
+ if (!Number.isInteger(l.leverage) || l.leverage < 1 || (l.marginMode !== "CROSS" && l.marginMode !== "ISOLATED")) {
850
+ throw new PerpError("EXCHANGE_ERROR", `Arcus /v1/leverages: unreadable entry ${JSON.stringify(l).slice(0, 160)}`);
851
+ }
852
+ out.push({ symbol: await this.symbolOf(l.marketId), leverage: l.leverage, marginMode: l.marginMode === "ISOLATED" ? "isolated" : "cross" });
853
+ }
854
+ return out;
855
+ }
856
+ async getOrderStatus(symbol, ref) {
857
+ const m = await this.market(symbol);
858
+ const r = this.orderResultFromRaw(symbol, await this.findOrder(m, ref));
859
+ // A resting order read back from the venue's order store is a settled observation of this
860
+ // read (it is on the book) — final, like the terminal states. Only an order the venue has
861
+ // merely accepted (not yet on the book) stays pending.
862
+ return r.status === "open" || r.status === "partiallyFilled" ? { ...r, finality: "final" } : r;
863
+ }
864
+ // ── Legacy: market data ──
865
+ async getMarkets() {
866
+ return (await this.getVenueMarkets()).map(toLegacyMarketInfo);
867
+ }
868
+ async getOrderbook(symbol) {
869
+ const m = await this.market(symbol);
870
+ const ob = await this.client.get(`/v1/l2OrderBook/${encodeURIComponent(m.marketDisplayName)}`, "l2OrderBook", { nLevels: ORDERBOOK_LEVELS });
871
+ return { bids: ob.bids, asks: ob.asks };
872
+ }
873
+ async getRecentTrades(symbol, limit = 50) {
874
+ const m = await this.market(symbol);
875
+ const res = await this.client.get("/v1/trades", "trades", { market: m.marketDisplayName, limit });
876
+ // Public tape carries no fee; "0" follows the legacy ExchangeTrade convention for public trades.
877
+ return res.trades.map(t => ({ time: usToMs(t.timestamp), symbol: m.baseAsset, side: sideLower(t.side), price: t.price, size: t.size, fee: "0" }));
878
+ }
879
+ async getFundingHistory(symbol, limit = 100) {
880
+ const m = await this.market(symbol);
881
+ const res = await this.client.get("/v1/fundingRates", "fundingRates", { market: m.marketDisplayName, limit });
882
+ return res.fundingRates.map(f => ({ time: usToMs(f.time), rate: f.fundingRate, price: null }));
883
+ }
884
+ async getKlines(symbol, interval, startTime, endTime) {
885
+ if (!CANDLE_TIMEFRAMES.has(interval)) {
886
+ throw new PerpError("INVALID_PARAMS", `Arcus: unsupported candle interval "${interval}"`, {
887
+ remediation: `Use one of: ${[...CANDLE_TIMEFRAMES].join(", ")}`,
888
+ });
889
+ }
890
+ const m = await this.market(symbol);
891
+ const res = await this.client.get("/v1/candles", "candles", { market: m.marketDisplayName, timeframe: interval, from: startTime * 1000, to: endTime * 1000 });
892
+ return res.candles
893
+ .map(c => ({ time: usToMs(c.openTime), open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume, trades: c.tradeCount }))
894
+ .sort((x, y) => x.time - y.time);
895
+ }
896
+ // ── Legacy: account ──
897
+ async rawPositions() {
898
+ const res = await this.client.get("/v1/positions", "positions", this.accountQuery());
899
+ return Object.values(res.positions);
900
+ }
901
+ async getBalance() {
902
+ const acct = await this.client.get("/v1/account", "account", this.accountQuery());
903
+ const positions = await this.rawPositions();
904
+ const upnl = positions.reduce((s, p) => s + Number(p.unrealizedPnl), 0);
905
+ return {
906
+ equity: acct.equity,
907
+ available: acct.freeCollateral,
908
+ // freeCollateral = equity − Σ initial margin required (docs), so margin used is the difference.
909
+ marginUsed: String(Number(acct.equity) - Number(acct.freeCollateral)),
910
+ unrealizedPnl: String(upnl),
911
+ };
912
+ }
913
+ async getPositions() {
914
+ const out = [];
915
+ for (const p of await this.rawPositions()) {
916
+ if (Number(p.size) === 0)
917
+ continue;
918
+ out.push({
919
+ symbol: await this.symbolOf(p.marketId),
920
+ side: p.side === "LONG" ? "long" : "short",
921
+ size: p.size.startsWith("-") ? p.size.slice(1) : p.size,
922
+ entryPrice: p.averageEntryPrice,
923
+ markPrice: p.markPx,
924
+ liquidationPrice: null, // Arcus does not publish a liquidation price
925
+ unrealizedPnl: p.unrealizedPnl,
926
+ leverage: Number(p.leverage),
927
+ });
928
+ }
929
+ return out;
930
+ }
931
+ async toLegacyOrders(orders) {
932
+ const out = [];
933
+ for (const o of orders) {
934
+ out.push({
935
+ orderId: o.orderId,
936
+ symbol: await this.symbolOf(o.marketId),
937
+ side: sideLower(o.side),
938
+ price: o.price,
939
+ size: o.originalSize,
940
+ filled: this.filledOf(o),
941
+ status: o.status,
942
+ type: o.tpslType ?? o.type ?? "LIMIT",
943
+ triggerPrice: o.triggerPrice !== undefined && Number(o.triggerPrice) > 0 ? o.triggerPrice : null,
944
+ });
945
+ }
946
+ return out;
947
+ }
948
+ async getOpenOrders() {
949
+ const res = await this.client.get("/v1/openOrders", "openOrders", this.accountQuery());
950
+ return this.toLegacyOrders(res.orders);
951
+ }
952
+ async getOrderHistory(limit = 100) {
953
+ const res = await this.client.get("/v1/orders", "orders", this.accountQuery({ limit }));
954
+ return this.toLegacyOrders(res.orders);
955
+ }
956
+ async getTradeHistory(limit = 100) {
957
+ const res = await this.client.get("/v1/fills", "fills", this.accountQuery({ limit }));
958
+ const out = [];
959
+ for (const f of res.fills) {
960
+ out.push({ time: usToMs(f.createdAt), symbol: (await this.market(f.marketDisplayName)).baseAsset, side: sideLower(f.side), price: f.price, size: f.size, fee: f.fee });
961
+ }
962
+ return out;
963
+ }
964
+ async getFundingPayments(limit = 100) {
965
+ const res = await this.client.get("/v1/funding", "funding", this.accountQuery({ limit }));
966
+ const out = [];
967
+ for (const p of res.fundingPayments)
968
+ out.push({ time: usToMs(p.time), symbol: await this.symbolOf(p.marketId), payment: p.payment });
969
+ return out;
970
+ }
971
+ // ── Legacy: trading shims (§10.7) ──
972
+ /** `clientOrderId`: the caller's id when given; otherwise one is generated (orderAck async needs one). */
973
+ async marketOrder(symbol, side, size, opts) {
974
+ return this.placeOrder({
975
+ symbol, side, type: "market", size, price: null, triggerPrice: null, triggerSource: null, tif: null,
976
+ reduceOnly: opts.reduceOnly, clientOrderId: opts.clientOrderId ?? newClientOrderId(),
977
+ slippage: opts.slippage, tpsl: null,
978
+ });
979
+ }
980
+ async limitOrder(symbol, side, price, size, opts) {
981
+ const tif = opts?.tif !== undefined ? validateLegacyTif(this.venue, this.caps, opts.tif) : null; // null → GTC (placeOrder)
982
+ return this.placeOrder({
983
+ symbol, side, type: "limit", size, price, triggerPrice: null, triggerSource: null, tif,
984
+ reduceOnly: opts?.reduceOnly === true, clientOrderId: opts?.clientOrderId ?? newClientOrderId(), slippage: null, tpsl: null,
985
+ });
986
+ }
987
+ async editOrder(symbol, orderId, price, size) {
988
+ return this.amendOrder({ symbol, ref: parseOrderRef(orderId), price, size, tif: null }); // keeps the resting TIF
989
+ }
990
+ async cancelOrder(symbol, orderId) {
991
+ return this.cancelByRef(symbol, parseOrderRef(orderId));
992
+ }
993
+ async cancelAllOrders(symbol) {
994
+ return this.cancelAllV2(symbol ?? null);
995
+ }
996
+ async setLeverage(symbol, leverage, marginMode) {
997
+ return this.applyLeverage(symbol, leverage, marginMode ?? null);
998
+ }
999
+ /** `opts.slippage` (resolveSlippage() output) is required for a stop-market: its leg is protection-priced at trigger ± bound. */
1000
+ async stopOrder(symbol, side, size, triggerPrice, opts) {
1001
+ if (opts?.triggerType !== undefined) {
1002
+ throw unsupported(this.venue.displayName, "triggerSource", `no trigger-price source choice (got --trigger-type ${opts.triggerType})`, "Omit --trigger-type for arcus");
1003
+ }
1004
+ const isLimit = opts?.limitPrice !== undefined;
1005
+ return this.placeOrder({
1006
+ symbol, side, type: isLimit ? "stopLimit" : "stopMarket", size,
1007
+ price: isLimit ? opts?.limitPrice : null,
1008
+ triggerPrice, triggerSource: null, // Arcus does not expose a trigger-source choice
1009
+ tif: isLimit ? "GTC" : null, // sent as GTT at the max horizon
1010
+ reduceOnly: opts?.reduceOnly === true, // non-reduce-only → CAPABILITY_UNSUPPORTED in validation
1011
+ clientOrderId: newClientOrderId(),
1012
+ // The caller's resolved bound (the adapter never reads settings, §13.10 #1);
1013
+ // a stop-market without it → INVALID_PARAMS in placeOrder.
1014
+ slippage: isLimit ? null : opts?.slippage ?? null,
1015
+ tpsl: null,
1016
+ });
1017
+ }
1018
+ // ── Funds ──
1019
+ masterSigner() {
1020
+ if (this.masterCreds)
1021
+ return OwsEvmSigner.create(this.masterCreds.wallet, this.masterCreds.passphrase ?? undefined);
1022
+ if (this.masterPk !== null) {
1023
+ return import("../../signer/evm-local.js").then(m => m.LocalEvmSigner.create(this.masterPk));
1024
+ }
1025
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Arcus withdrawals need the master wallet signature, which is not on this computer; the agent API key is trade-only", {
1026
+ remediation: "Withdraw in the Arcus app, or add the master wallet (perp wallet import --name <name> --chain evm, then --wallet <name>)",
1027
+ });
1028
+ }
1029
+ requireMasterKey() {
1030
+ if (this.masterCreds === null && this.masterPk === null) {
1031
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Arcus spot swaps are signed by the master EVM wallet, and none is configured", {
1032
+ remediation: "Pass the master OWS wallet: --wallet <name>",
1033
+ });
1034
+ }
1035
+ }
1036
+ /** Master EVM signer for on-chain actions outside the Arcus API (spot swaps, design §12.4 #7). */
1037
+ async masterEvmSigner() {
1038
+ this.requireMasterKey();
1039
+ return this.masterSigner();
1040
+ }
1041
+ /** Sign an unsigned EVM transaction with the master key (digest derived here). Returns a 65-byte 0x signature. */
1042
+ async signMasterTransaction(tx) {
1043
+ this.requireMasterKey();
1044
+ const { unsignedTxDigest } = await import("../../signer/ows-evm.js");
1045
+ const digest = unsignedTxDigest(tx);
1046
+ if (this.masterCreds) {
1047
+ const { loadOws } = await import("../../signer/ows-loader.js");
1048
+ const { canonicalizeOwsSignature } = await import("../../signer/ows-evm.js");
1049
+ return canonicalizeOwsSignature(loadOws().signHash(this.masterCreds.wallet, "ethereum", digest, this.masterCreds.passphrase ?? undefined));
1050
+ }
1051
+ const { SigningKey } = await import("ethers");
1052
+ const pk = this.masterPk;
1053
+ return new SigningKey(pk.startsWith("0x") ? pk : `0x${pk}`).sign(digest).serialized;
1054
+ }
1055
+ /** The only valid withdraw destination: the account owner (needs the agent API key). */
1056
+ get withdrawDestination() {
1057
+ return this.requireAccount().owner;
1058
+ }
1059
+ /** Withdraw-to-self (Arcus v0 has no separate recipient). `amount` in USDG. */
1060
+ async withdraw(amount, destination, _opts) {
1061
+ const a = this.requireAccount();
1062
+ if (destination.toLowerCase() !== a.owner.toLowerCase()) {
1063
+ throw new PerpError("INVALID_PARAMS", `Arcus only withdraws to the account owner ${a.owner}`, {
1064
+ remediation: `Use destination ${a.owner}`,
1065
+ });
1066
+ }
1067
+ // Arcus withdraw amounts carry at most 6 decimals (USDG); reject before signing anything.
1068
+ if (!/^\d+(\.\d{1,6})?$/.test(amount)) {
1069
+ throw new PerpError("INVALID_PARAMS", `Arcus withdraw amount must be a plain decimal with at most 6 decimals (got ${amount})`, {
1070
+ remediation: "Round the amount to 6 decimals (USDG precision)",
1071
+ });
1072
+ }
1073
+ const quantums = toUnitsExact(amount, QUOTE_QUANTUM, "amount");
1074
+ if (quantums < 1000000000n) {
1075
+ throw new PerpError("SIZE_TOO_SMALL", `Arcus minimum withdrawal is 1 USDG (got ${amount})`);
1076
+ }
1077
+ const signer = await this.masterSigner();
1078
+ if (signer.getAddress().toLowerCase() !== a.owner.toLowerCase()) {
1079
+ throw new PerpError("INVALID_PARAMS", `Master wallet ${signer.getAddress()} does not own Arcus account ${a.owner}`);
1080
+ }
1081
+ const chainId = networkConfig(this.venue, this.network).signingChainId;
1082
+ if (chainId === null)
1083
+ throw new Error("arcus venue definition is missing signingChainId");
1084
+ const nonce = nowNs().toString(); // Unix-ns, monotonic per process (docs: nonce = timestamp string)
1085
+ const td = withdrawTypedData({ chainId, ethereumAddress: a.owner, accountIndex: a.sub, amountQuantums: quantums, nonce });
1086
+ const signature = splitEvmSignature(await signer.signTypedData(td.domain, td.types, td.message));
1087
+ const res = await this.client.postUnsigned("/v1/withdraw", "withdraw", {
1088
+ ethereumAddress: a.owner, accountIndex: a.sub, amount: quantums.toString(), nonce, signature,
1089
+ });
1090
+ return res.data;
1091
+ }
1092
+ }