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,273 @@
1
+ /**
2
+ * Spot order outcome → filled size, for bots / arb (design §12.4 #5).
3
+ *
4
+ * A resolved `spotMarketOrder()` promise does NOT mean "filled": the §12 adapters
5
+ * return a `SpotOrderResult` (rejected / cancelled / pending / partial / filled),
6
+ * and the legacy Hyperliquid / Lighter adapters return a venue payload. Every bot /
7
+ * arb spot leg goes through `spotMarketFill()` so the hedge is sized to what
8
+ * actually filled:
9
+ *
10
+ * - rejected / cancelled (nothing filled) → throws ORDER_REJECTED
11
+ * - pending / open / fill size not reported → throws ORDER_PENDING (do NOT retry)
12
+ * - the order call itself throws → a PerpError with a definite code
13
+ * (isDefiniteOrderFailure: validation / policy / explicit venue rejection) is
14
+ * rethrown as a definite failure; ANYTHING else (non-PerpError, EXCHANGE_UNREACHABLE,
15
+ * TIMEOUT, EXCHANGE_ERROR, RATE_LIMITED, UNKNOWN, …) becomes ORDER_PENDING — the
16
+ * request may have reached the venue, so callers quarantine instead of retrying.
17
+ * - partial → returns the filled size
18
+ * - filled → returns the filled size
19
+ *
20
+ * Legacy results:
21
+ * - Hyperliquid: `response.data.statuses[0].filled.totalSz` (resting → pending).
22
+ * - Lighter: the adapter reads the fill from Lighter's own /tx event_info (taker
23
+ * order "filled") or, for any other status, the order's accountOrders row
24
+ * (filled_base_amount), and returns a final SpotOrderResult that iocOutcomeOf() reports
25
+ * (venueFinalSpotFill below). A SpotOrderResult marked TX_EVENT_FILL_SOURCE that is not
26
+ * routed through iocOutcomeOf is cross-checked as follows; because
27
+ * the is/rs semantics are not venue-documented, that fill must be backed by a fresh
28
+ * base-balance delta of ≥ 80% (else ORDER_PENDING). Only when the venue gives no fill
29
+ * report does it return the raw tx hash; the fill is then MEASURED as the base-token
30
+ * balance delta around the order using UNCACHED reads (readSpotBalancesFresh). A
31
+ * zero delta is ORDER_PENDING (the IOC may still be unconfirmed), never "nothing
32
+ * filled"; a failed read is never zero.
33
+ *
34
+ * Venue-final IOC outcome (design §13.10 #4): when the spot adapter is IocOutcomeCapable
35
+ * and iocOutcomeOf(result) reports the order's FINAL filled size, that report is used in
36
+ * preference to the balance delta, cross-checked against a fresh base-balance delta:
37
+ * - final 0 and the balance did not move (≤ one lot) → ORDER_REJECTED (definite);
38
+ * the balance DID move → the measured delta is used (discrepancy logged to stderr);
39
+ * - final > 0 → filledSize = the reported size when the delta is ≥ 80% of it (base-token
40
+ * fees), else ORDER_PENDING; an unreadable balance → ORDER_PENDING.
41
+ */
42
+ import { PerpError, isDefiniteOrderFailure, orderOutcomeUnknown } from "../errors.js";
43
+ import { resolveAdapterSlippage } from "./slippage.js";
44
+ import { iocLotTolerance, isIocOutcomeCapable } from "./ioc-outcome.js";
45
+ import { isSpotFreshBalancesCapable, TX_EVENT_FILL_SOURCE } from "./spot-interface.js";
46
+ import { spotTokenBase } from "./spot-factory.js";
47
+ /** A tx-event-derived / venue-final fill must be backed by at least this share of balance movement (base-token fees). */
48
+ const TX_FILL_MIN_BALANCE_RATIO = 0.8;
49
+ function isTxEventFill(r) {
50
+ const raw = r.venueRaw;
51
+ return raw !== null && typeof raw === "object" && raw.fillSource === TX_EVENT_FILL_SOURCE;
52
+ }
53
+ /** A fill below requested × (1 − this) is reported as "partial" (step flooring dust excluded). */
54
+ const FULL_FILL_TOLERANCE = 1e-6;
55
+ function isSpotOrderResult(r) {
56
+ if (r === null || typeof r !== "object")
57
+ return false;
58
+ const o = r;
59
+ return typeof o.status === "string" && (o.finality === "final" || o.finality === "pending") && "filledSize" in o;
60
+ }
61
+ function pendingError(venue, symbol, why, orderId, remediation, raw) {
62
+ return new PerpError("ORDER_PENDING", `${venue} spot ${symbol}: ${why}${orderId !== null ? ` (order ${orderId})` : ""}`, {
63
+ exchange: venue,
64
+ orderId,
65
+ raw,
66
+ remediation: remediation ?? `Do NOT resubmit — the order may still fill. Check: perp -e ${venue} trade spot-balance / spot-orders`,
67
+ });
68
+ }
69
+ function positive(v) {
70
+ if (v === null || v === undefined)
71
+ return null;
72
+ const n = Number(v);
73
+ return Number.isFinite(n) && n > 0 ? n : null;
74
+ }
75
+ function classify(filled, requested) {
76
+ return filled >= requested * (1 - FULL_FILL_TOLERANCE) ? "filled" : "partial";
77
+ }
78
+ /**
79
+ * Interpret a spot order result. `measuredFill` = base balance delta measured around
80
+ * the order (null when not measured); used only for results that carry no fill report.
81
+ */
82
+ export function interpretSpotFill(venue, symbol, result, requestedSize, measuredFill) {
83
+ if (isSpotOrderResult(result)) {
84
+ const r = result;
85
+ if (r.status === "rejected" || r.status === "cancelled") {
86
+ // "cancelled" with a fill would be "partial" (adapters map it so); here nothing filled.
87
+ throw new PerpError("ORDER_REJECTED", `${venue} spot ${symbol}: order ${r.status}${r.remediation ? ` — ${r.remediation}` : ""}`, {
88
+ exchange: venue, orderId: r.orderId, raw: r.venueRaw,
89
+ });
90
+ }
91
+ if (r.finality === "pending" || r.status === "pending" || r.status === "open") {
92
+ throw pendingError(venue, symbol, `order not final (status ${r.status})`, r.orderId, r.remediation, r.venueRaw);
93
+ }
94
+ const filled = positive(r.filledSize);
95
+ if (filled === null) {
96
+ throw pendingError(venue, symbol, `order ${r.status} but the venue reported no filled size`, r.orderId, null, r.venueRaw);
97
+ }
98
+ return {
99
+ status: r.status === "partial" ? "partial" : classify(filled, requestedSize),
100
+ filledSize: filled,
101
+ avgPrice: positive(r.avgPrice),
102
+ orderId: r.orderId,
103
+ raw: result,
104
+ };
105
+ }
106
+ // Legacy Hyperliquid: { status: "ok", response: { data: { statuses: [{ filled | resting | error }] } } }
107
+ const hl = result;
108
+ const statuses = hl?.response?.data?.statuses;
109
+ if (Array.isArray(statuses)) {
110
+ const st = statuses[0];
111
+ if (st === undefined)
112
+ throw pendingError(venue, symbol, "no order status in the response", null, null, result);
113
+ if (st.error)
114
+ throw new PerpError("ORDER_REJECTED", `${venue} spot ${symbol}: ${String(st.error)}`, { exchange: venue });
115
+ const f = st.filled;
116
+ const filled = positive(f?.totalSz);
117
+ if (filled === null) {
118
+ const resting = st.resting;
119
+ throw pendingError(venue, symbol, "order not filled (resting or unreported)", resting?.oid !== undefined ? String(resting.oid) : null, null, result);
120
+ }
121
+ return { status: classify(filled, requestedSize), filledSize: filled, avgPrice: positive(f?.avgPx), orderId: f?.oid !== undefined ? String(f.oid) : null, raw: result };
122
+ }
123
+ // Legacy Lighter (tx hash only): measured balance delta.
124
+ if (measuredFill === null) {
125
+ throw pendingError(venue, symbol, "the venue reported no fill and none was measured", null, null, result);
126
+ }
127
+ if (!(measuredFill > 0)) {
128
+ throw pendingError(venue, symbol, "no balance change observed after the order (it may still be unconfirmed)", null, null, result);
129
+ }
130
+ return { status: classify(measuredFill, requestedSize), filledSize: Math.min(measuredFill, requestedSize), avgPrice: null, orderId: null, raw: result };
131
+ }
132
+ /**
133
+ * Uncached spot balances: getSpotBalancesFresh() on adapters whose getSpotBalances()
134
+ * is TTL-cached (SpotFreshBalancesCapable contract), getSpotBalances() on adapters that
135
+ * read live. Use for fill measurement and post-trade verification. Throws on failure.
136
+ */
137
+ export async function readSpotBalancesFresh(spot) {
138
+ return isSpotFreshBalancesCapable(spot) ? spot.getSpotBalancesFresh() : spot.getSpotBalances();
139
+ }
140
+ /** Base-token balance (total) on the spot venue, read uncached; a token with no balance row holds 0. */
141
+ async function baseBalance(spot, symbol) {
142
+ const base = spotTokenBase(symbol.split("/")[0] ?? symbol);
143
+ const row = (await readSpotBalancesFresh(spot)).find(b => spotTokenBase(b.token) === base);
144
+ if (row === undefined)
145
+ return 0;
146
+ const n = Number(row.total);
147
+ if (!Number.isFinite(n))
148
+ throw new PerpError("EXCHANGE_ERROR", `${spot.name} spot balance for ${base} is not a number: ${row.total}`, { exchange: spot.name });
149
+ return n;
150
+ }
151
+ /**
152
+ * Place a spot market order and return what filled (see header). The base balance is
153
+ * read before the order so venues without a fill report can be measured; a failure of
154
+ * that read aborts BEFORE anything is placed. After the order, errors never throw a
155
+ * retryable error: an unreadable outcome is ORDER_PENDING.
156
+ */
157
+ export async function spotMarketFill(spot, symbol, side, size, stepSize = null) {
158
+ const requested = Number(size);
159
+ if (!(requested > 0))
160
+ throw new PerpError("INVALID_PARAMS", `spot ${side} size must be positive (got ${size})`);
161
+ // Bot/arb spot legs carry no per-call flag: settings slippageBps[venue] → defaultSlippageBps.
162
+ const slippage = resolveAdapterSlippage(spot, null);
163
+ const before = await baseBalance(spot, symbol);
164
+ let result;
165
+ try {
166
+ result = await spot.spotMarketOrder(symbol, side, size, { slippage });
167
+ }
168
+ catch (e) {
169
+ if (isDefiniteOrderFailure(e))
170
+ throw e;
171
+ // Not a definite rejection: the request may have reached the venue.
172
+ throw orderOutcomeUnknown(spot.name, `spot ${side} ${size} ${symbol}`, e, `Do NOT resubmit — the order may still fill. Check: perp -e ${spot.name} trade spot-balance / spot-orders`);
173
+ }
174
+ const outcome = isIocOutcomeCapable(spot) ? spot.iocOutcomeOf(result) : null;
175
+ if (outcome !== null)
176
+ return venueFinalSpotFill(spot, symbol, side, size, before, result, outcome, stepSize);
177
+ if (isSpotOrderResult(result)) {
178
+ const fill = interpretSpotFill(spot.name, symbol, result, requested, null);
179
+ if (!isTxEventFill(result))
180
+ return fill;
181
+ // Lighter-family fills are derived from the tx event_info (`is` − `rs`), whose
182
+ // semantics are not venue-documented: cross-check against a FRESH balance delta.
183
+ let after;
184
+ try {
185
+ after = await baseBalance(spot, symbol);
186
+ }
187
+ catch (e) {
188
+ throw pendingError(spot.name, symbol, `tx reported ${fill.filledSize} filled but the post-order balance could not be read (${e instanceof Error ? e.message : String(e)})`, fill.orderId, null, result);
189
+ }
190
+ const delta = side === "buy" ? after - before : before - after;
191
+ if (delta < fill.filledSize * TX_FILL_MIN_BALANCE_RATIO) {
192
+ throw pendingError(spot.name, symbol, `tx reported ${fill.filledSize} filled but the balance moved only ${delta}`, fill.orderId, null, result);
193
+ }
194
+ return fill;
195
+ }
196
+ const hl = result;
197
+ if (Array.isArray(hl?.response?.data?.statuses))
198
+ return interpretSpotFill(spot.name, symbol, result, requested, null);
199
+ let after;
200
+ try {
201
+ after = await baseBalance(spot, symbol);
202
+ }
203
+ catch (e) {
204
+ throw pendingError(spot.name, symbol, `order sent but the post-order balance could not be read (${e instanceof Error ? e.message : String(e)})`, null, null, result);
205
+ }
206
+ const delta = side === "buy" ? after - before : before - after;
207
+ return interpretSpotFill(spot.name, symbol, result, requested, delta);
208
+ }
209
+ /** spotMarketFill for a venue-final IOC outcome (see header): the reported fill, cross-checked against the balance delta. */
210
+ async function venueFinalSpotFill(spot, symbol, side, size, before, result, outcome, stepSize) {
211
+ const requested = Number(size);
212
+ const hlStatus = result?.response?.data?.statuses?.[0];
213
+ const hlFilled = hlStatus?.filled;
214
+ const orderId = isSpotOrderResult(result) ? result.orderId : hlFilled?.oid !== undefined ? String(hlFilled.oid) : null;
215
+ const reported = Number(outcome.filledSize);
216
+ if (!(Number.isFinite(reported) && reported >= 0)) {
217
+ throw pendingError(spot.name, symbol, `venue reported an unusable final fill "${outcome.filledSize}"`, orderId, null, result);
218
+ }
219
+ const tol = iocLotTolerance(size, outcome.filledSize, stepSize);
220
+ if (reported > requested + tol) {
221
+ throw pendingError(spot.name, symbol, `venue reported a final fill of ${outcome.filledSize}, more than the ${size} ordered`, orderId, null, result);
222
+ }
223
+ let after;
224
+ try {
225
+ after = await baseBalance(spot, symbol);
226
+ }
227
+ catch (e) {
228
+ throw pendingError(spot.name, symbol, `venue reported a final fill of ${outcome.filledSize} but the post-order balance could not be read (${e instanceof Error ? e.message : String(e)})`, orderId, null, result);
229
+ }
230
+ const delta = side === "buy" ? after - before : before - after;
231
+ if (reported === 0) {
232
+ if (!(delta > tol)) {
233
+ const venueError = hlStatus?.error;
234
+ throw new PerpError("ORDER_REJECTED", `${spot.name} spot ${symbol}: ${venueError ? String(venueError) : "IOC filled nothing"} (venue-final, balance unchanged)`, {
235
+ exchange: spot.name, orderId, raw: result,
236
+ });
237
+ }
238
+ process.stderr.write(`[warn] ${spot.name} spot ${side} ${size} ${symbol}: venue reported a final fill of 0 but the balance moved ${delta} — using the measured fill\n`);
239
+ return { status: classify(delta, requested), filledSize: Math.min(delta, requested), avgPrice: null, orderId, raw: result };
240
+ }
241
+ if (delta < reported * TX_FILL_MIN_BALANCE_RATIO) {
242
+ throw pendingError(spot.name, symbol, `venue reported a final fill of ${outcome.filledSize} but the balance moved only ${delta}`, orderId, null, result);
243
+ }
244
+ const avgPrice = isSpotOrderResult(result) ? positive(result.avgPrice) : positive(hlFilled?.avgPx);
245
+ return { status: classify(reported, requested), filledSize: Math.min(reported, requested), avgPrice, orderId, raw: result };
246
+ }
247
+ /** Decimal string for a filled size (no exponent), floored to `decimals`. */
248
+ export function fillSizeString(filled, decimals) {
249
+ const f = 10 ** decimals;
250
+ return (Math.floor(filled * f + 1e-9) / f).toFixed(decimals);
251
+ }
252
+ /** Decimal places of a plain decimal size string ("0.0100" → 4). */
253
+ export function sizeDecimalsOf(size) {
254
+ const frac = size.split(".")[1];
255
+ return frac === undefined ? 0 : frac.length;
256
+ }
257
+ /**
258
+ * Spot lot step (10^-sizeDecimals) from the venue's spot market info. `symbol` is "BASE/QUOTE"
259
+ * or a bare base; a bare base whose markets disagree on the lot, or an unknown market, throws
260
+ * (Rule #2: never size off the typed decimals).
261
+ */
262
+ export async function spotLotStepOf(spot, symbol) {
263
+ const want = symbol.toUpperCase();
264
+ const markets = (await spot.getSpotMarkets()).filter(m => want.includes("/")
265
+ ? m.symbol.toUpperCase() === want
266
+ : spotTokenBase(m.baseToken) === spotTokenBase(want));
267
+ const decs = [...new Set(markets.map(m => m.sizeDecimals))];
268
+ if (decs.length !== 1 || !Number.isInteger(decs[0]) || decs[0] < 0) {
269
+ throw new PerpError(markets.length === 0 ? "SYMBOL_NOT_FOUND" : "EXCHANGE_ERROR", `${spot.name} spot ${symbol}: ${markets.length === 0 ? "market not found" : `ambiguous / unusable lot precision (${decs.join(", ")})`} — lot step unknown`, { exchange: spot.name });
270
+ }
271
+ const d = decs[0];
272
+ return d === 0 ? "1" : `0.${"0".repeat(d - 1)}1`;
273
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * TradingV2 — normalized order entry for venues added under the venue
3
+ * abstraction (docs/DESIGN-venue-abstraction.md §3 L3, §10.7, §10.10).
4
+ *
5
+ * Method names deliberately do not overlap the legacy ExchangeAdapter trading
6
+ * methods, so an adapter can implement both without overloads.
7
+ */
8
+ import { PerpError } from "../errors.js";
9
+ import type { VenueCapabilities, VenueDefinition } from "./venue.js";
10
+ import type { AmendRequest, CancelResult, LeverageResult, Network, OrderRef, OrderRequest, OrderResult, TimeInForce, VenueId, VenueMarket } from "./venue-types.js";
11
+ export interface VenueBase {
12
+ readonly name: VenueId;
13
+ readonly venue: VenueDefinition;
14
+ readonly network: Network;
15
+ }
16
+ export interface VenueMarketsSource extends VenueBase {
17
+ getVenueMarkets(): Promise<VenueMarket[]>;
18
+ }
19
+ export interface TradingV2 extends VenueMarketsSource {
20
+ placeOrder(req: OrderRequest): Promise<OrderResult>;
21
+ amendOrder(req: AmendRequest): Promise<OrderResult>;
22
+ cancelByRef(symbol: string, ref: OrderRef): Promise<CancelResult>;
23
+ cancelAllV2(symbol: string | null): Promise<CancelResult>;
24
+ applyLeverage(symbol: string, leverage: number, marginMode: "cross" | "isolated" | null): Promise<LeverageResult>;
25
+ getOrderStatus(symbol: string, ref: OrderRef): Promise<OrderResult>;
26
+ }
27
+ export declare function isTradingV2(a: unknown): a is TradingV2;
28
+ /**
29
+ * Uniform "not supported" response (design §13.6, §13.10 #13): CAPABILITY_UNSUPPORTED
30
+ * with details { venue, feature, reason } and an alternative in `remediation`.
31
+ * `venue` is a venue id or display name; `feature` a short capability name
32
+ * ("limitOrder", "fok", "postOnly", "amend", "trading", "orderbook", ...).
33
+ */
34
+ export declare function unsupported(venue: string, feature: string, reason: string, remediation: string): PerpError;
35
+ /**
36
+ * An adapter whose reduce-only orders are currently refused (a venue whose reduce-only wire
37
+ * format is not set; none today) reports why. Callers that will need a reduce-only order
38
+ * LATER in a flow ask first — an entry carrying --tp/--sl is refused BEFORE the entry is sent
39
+ * (never "entry filled, protection failed"), and a close is refused before any read. Adapters
40
+ * without the method have no such gate.
41
+ */
42
+ export declare function reduceOnlyGateOf(adapter: unknown): PerpError | null;
43
+ /**
44
+ * An adapter whose STOP orders are currently refused (QFEX until stop acknowledgements can be
45
+ * correlated) reports why. `--tp/--sl` entries and `trade tpsl` ask first, so an entry is never
46
+ * sent when its protective legs would be refused. Adapters without the method have no such gate.
47
+ */
48
+ export declare function stopOrderGateOf(adapter: unknown): PerpError | null;
49
+ /**
50
+ * An adapter on which UNATTENDED automation is currently refused (QFEX while its stop orders are
51
+ * refused) reports why. Bots, plans, TWAP, DCA, grid, funding-arb, trailing stop and background
52
+ * jobs ask before they start; manual commands never ask. Adapters without the method have no
53
+ * such gate.
54
+ */
55
+ export declare function automationGateOf(adapter: unknown): PerpError | null;
56
+ /**
57
+ * Refuse to START anything that trades unattended on a venue that cannot close or protect its
58
+ * positions. Bots, plans, TWAP, DCA, grid, funding-arb, trailing stop and background jobs all
59
+ * need a reduce-only order later (close / stop / drawdown exit): on a venue whose reduce-only
60
+ * orders are gated (reduceOnlyGateOf) they would open a position they cannot close, and on a
61
+ * venue that refuses automation (automationGateOf) they would run without a protective exit.
62
+ * Throws the gate before the first order.
63
+ */
64
+ export declare function assertCanClosePositions(adapter: unknown, what: string): void;
65
+ /** The gate's own code / details / remediation, with what was refused and why in front. */
66
+ export declare function startRefusal(what: string, why: string, gate: PerpError): PerpError;
67
+ /**
68
+ * Validate an order request against the venue's declared capabilities BEFORE
69
+ * anything is sent. Throws; never adjusts the request.
70
+ */
71
+ export declare function validateOrderRequest(venue: VenueDefinition, caps: VenueCapabilities, req: OrderRequest): void;
72
+ /**
73
+ * Resolve a CLI TIF input (common GTC/IOC/FOK/ALO or a venue alias GTT/GTX/TOB,
74
+ * case-insensitive) to a value the venue declares in its capabilities. Never
75
+ * substitutes a different meaning: FOK is never emulated (§13.3) and an
76
+ * unknown / unoffered value is the uniform CAPABILITY_UNSUPPORTED response.
77
+ */
78
+ export declare function validateLegacyTif(venue: VenueDefinition, caps: VenueCapabilities, tif: string): TimeInForce;
79
+ /**
80
+ * Poll getOrderStatus until the order reaches a final state, is resting ("open"),
81
+ * or the timeout elapses. On timeout returns the last observation with
82
+ * finality "pending" — never guesses a fill.
83
+ *
84
+ * `untilResting`: treat "open"/"partiallyFilled" as a stopping point (limit orders).
85
+ * `ack`: the placement acknowledgement of an async venue. When set, an
86
+ * ORDER_NOT_FOUND from getOrderStatus inside this loop means "acknowledged but
87
+ * not yet visible on the read path" (Arcus forwards to the engine before the
88
+ * order store has it) and the ack itself (status "accepted") stands as the
89
+ * observation. Outside this loop ORDER_NOT_FOUND is never reinterpreted.
90
+ */
91
+ export declare function awaitOrderFinal(adapter: TradingV2, symbol: string, ref: OrderRef, opts: {
92
+ timeoutMs: number;
93
+ pollMs: number;
94
+ untilResting: boolean;
95
+ ack: OrderResult | null;
96
+ }): Promise<OrderResult>;
97
+ /** Generate a client order id: time-ordered, 20 chars max, [0-9a-z]. */
98
+ export declare function newClientOrderId(prefix?: string): string;
99
+ /**
100
+ * Client order id in the venue's format: Lighter-family `clientOrderIndex` is a
101
+ * decimal integer ≤ 2^48 − 1 (ms timestamp × 100 + 2 random digits); QFEX and Pacifica take a
102
+ * UUID (QFEX: the format the venue itself generates; Pacifica: "Full UUID string"); Hyperliquid a
103
+ * 128-bit cloid ("0x" + 32 hex); every other venue (Aster: [.A-Z:/a-z0-9_-]{1,36}) takes
104
+ * newClientOrderId().
105
+ */
106
+ export declare function newVenueClientOrderId(venue: VenueDefinition): string;
107
+ /** OrderResult plus the caller-facing next step when the outcome is not final (null when final). */
108
+ export type PlacedOrder = OrderResult & {
109
+ remediation: string | null;
110
+ };
111
+ /**
112
+ * Place through TradingV2 and settle the outcome as far as the venue can prove it:
113
+ * async-ack venues are polled with awaitOrderFinal until final / resting (non-market)
114
+ * or ASYNC_ACK_TIMEOUT_MS. A non-final result carries a "do NOT resubmit" remediation;
115
+ * a failed status poll after the ack is never reported as a failed order.
116
+ */
117
+ export declare function placeAndConfirm(adapter: TradingV2, caps: VenueCapabilities, req: OrderRequest): Promise<PlacedOrder>;