perp-cli 0.12.17 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) 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 +286 -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 +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  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 +316 -337
  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 +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  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 +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -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 +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -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 +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  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 +1383 -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 +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -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 +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -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 +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +142 -55
  348. package/skills/perp-cli/references/agent-operations.md +110 -56
  349. package/skills/perp-cli/references/commands.md +266 -71
  350. package/skills/perp-cli/references/strategies.md +34 -31
  351. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  352. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -0,0 +1,396 @@
1
+ /**
2
+ * `perp outcome` — Hyperliquid Outcome markets (HIP-4).
3
+ *
4
+ * Currently HL-only. Asset class is fully collateralized binary/range
5
+ * contracts, quoted per outcome (outcomeMeta quoteToken, e.g. USDC). No leverage, no liquidation. Settlement is
6
+ * dated per-outcome.
7
+ */
8
+ import { isDryRun, supportsDryRun } from "../cli-flags.js";
9
+ import chalk from "chalk";
10
+ import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
11
+ import { HyperliquidOutcomeAdapter } from "../exchanges/hyperliquid-outcome.js";
12
+ import { makeTable, printJson, jsonOk, formatUsd } from "../utils.js";
13
+ import { PerpError } from "../errors.js";
14
+ export function registerOutcomeCommands(program, getAdapterForExchange, isJson) {
15
+ const outcome = program
16
+ .command("outcome")
17
+ .description("Hyperliquid Outcome markets (HIP-4) — binary/range contracts");
18
+ async function getOutcomeAdapter() {
19
+ const adapter = await getAdapterForExchange("hyperliquid");
20
+ if (!(adapter instanceof HyperliquidAdapter)) {
21
+ throw new PerpError("NOT_IMPLEMENTED", "Outcome markets are only available on Hyperliquid.", {
22
+ exchange: "hyperliquid",
23
+ remediation: "Run with -e hyperliquid (or omit -e to use the default).",
24
+ });
25
+ }
26
+ const out = new HyperliquidOutcomeAdapter(adapter);
27
+ await out.init();
28
+ return out;
29
+ }
30
+ // ── outcome list ─────────────────────────────────────────────────────────
31
+ outcome
32
+ .command("list")
33
+ .alias("markets")
34
+ .description("List active outcome markets")
35
+ .action(async () => {
36
+ const adapter = await getOutcomeAdapter();
37
+ const markets = await adapter.getMarkets();
38
+ if (isJson())
39
+ return printJson(jsonOk(markets));
40
+ if (markets.length === 0) {
41
+ console.log(chalk.gray("\n No active outcome markets.\n"));
42
+ return;
43
+ }
44
+ console.log(chalk.white.bold(`\n Outcome Markets (Hyperliquid HIP-4)`));
45
+ const quotes = [...new Set(markets.map((m) => m.quoteToken))].join(" / ");
46
+ console.log(chalk.gray(` Quote: ${quotes} (per outcome) · No leverage · Min order 10 quote tokens\n`));
47
+ for (const m of markets) {
48
+ const expiry = m.expiryMs ? new Date(m.expiryMs).toISOString().replace("T", " ").slice(0, 16) + " UTC" : "—";
49
+ console.log(` ${chalk.cyan(`#${m.outcome}`)} ${chalk.white.bold(m.name)} ${chalk.gray(`(${m.class ?? "?"} · ${m.underlying ?? "?"} · ${m.period ?? "?"})`)}`);
50
+ if (m.targetPrice !== undefined)
51
+ console.log(` target: $${m.targetPrice.toLocaleString()} expiry: ${expiry}`);
52
+ const sideRows = m.sides.map((s) => [
53
+ String(s.side),
54
+ chalk.white.bold(s.name),
55
+ s.mid ? `$${Number(s.mid).toFixed(4)}` : chalk.gray("—"),
56
+ chalk.gray(`assetId=${s.assetId}`),
57
+ ]);
58
+ console.log(makeTable(["#", "side", "mid", ""], sideRows));
59
+ console.log("");
60
+ }
61
+ });
62
+ // ── outcome book <outcome> <side> ────────────────────────────────────────
63
+ outcome
64
+ .command("book <outcome> <side>")
65
+ .description("Show orderbook for one outcome side (e.g., 'outcome book 1 0' or '1 yes')")
66
+ .option("--depth <n>", "Number of levels per side", "10")
67
+ .action(async (outcomeArg, sideArg, opts) => {
68
+ const adapter = await getOutcomeAdapter();
69
+ const { outcome: outcomeId, side } = await resolveOutcomeSide(adapter, outcomeArg, sideArg);
70
+ const book = await adapter.getOrderbook(outcomeId, side);
71
+ const depth = Math.max(1, Number(opts.depth ?? "10"));
72
+ if (isJson())
73
+ return printJson(jsonOk(book));
74
+ printOutcomeBook(book, depth);
75
+ });
76
+ // ── outcome view <outcome> ───────────────────────────────────────────────
77
+ // Symmetric Yes/No book + underlying gap + time to expiry. Single
78
+ // round-trip view for binary markets.
79
+ outcome
80
+ .command("view <outcome>")
81
+ .alias("status")
82
+ .description("Combined view: Yes/No books side-by-side + underlying mark gap + expiry")
83
+ .option("--depth <n>", "Number of levels per side", "10")
84
+ .action(async (outcomeArg, opts) => {
85
+ const adapter = await getOutcomeAdapter();
86
+ const outcomeId = Number(outcomeArg);
87
+ if (!Number.isInteger(outcomeId) || outcomeId < 0) {
88
+ throw new PerpError("INVALID_PARAMS", `Invalid outcome id: ${outcomeArg}`, {});
89
+ }
90
+ const depth = Math.max(1, Number(opts.depth ?? "10"));
91
+ const view = await adapter.getView(outcomeId, depth);
92
+ if (isJson())
93
+ return printJson(jsonOk(view));
94
+ printOutcomeView(view);
95
+ });
96
+ // ── outcome positions ────────────────────────────────────────────────────
97
+ outcome
98
+ .command("positions")
99
+ .description("Show open outcome positions")
100
+ .action(async () => {
101
+ const adapter = await getOutcomeAdapter();
102
+ const positions = await adapter.getPositions();
103
+ if (isJson())
104
+ return printJson(jsonOk(positions));
105
+ if (positions.length === 0) {
106
+ console.log(chalk.gray("\n No open outcome positions.\n"));
107
+ return;
108
+ }
109
+ const rows = positions.map((p) => [
110
+ chalk.cyan(`#${p.outcome}`),
111
+ chalk.white.bold(p.sideName),
112
+ p.size,
113
+ `$${formatUsd(p.entryNotional)}`,
114
+ p.markPrice ? `$${Number(p.markPrice).toFixed(4)}` : chalk.gray("—"),
115
+ formatPnlCell(p.unrealizedPnl),
116
+ ]);
117
+ console.log("\n" + makeTable(["outcome", "side", "size", "entry$", "mark", "uPnL"], rows) + "\n");
118
+ });
119
+ // ── outcome orders ───────────────────────────────────────────────────────
120
+ outcome
121
+ .command("orders")
122
+ .description("Show open outcome orders")
123
+ .action(async () => {
124
+ const adapter = await getOutcomeAdapter();
125
+ const orders = await adapter.getOpenOrders();
126
+ if (isJson()) {
127
+ return printJson(jsonOk(orders.map((o) => ({ ...o, outcome: adapter.describeCoin(String(o.symbol ?? "")) }))));
128
+ }
129
+ if (orders.length === 0) {
130
+ console.log(chalk.gray("\n No open outcome orders.\n"));
131
+ return;
132
+ }
133
+ // HL adapter normalises open-order sides to "buy"/"sell" (see
134
+ // HyperliquidAdapter.getOpenOrders → ExchangeOrder shape). Earlier
135
+ // versions checked the raw "B"/"A" form, which mislabelled every buy
136
+ // as SELL.
137
+ const rows = orders.map((o) => {
138
+ const side = String(o.side ?? "").toLowerCase();
139
+ const sideLabel = side === "buy" ? chalk.green("BUY") : side === "sell" ? chalk.red("SELL") : chalk.gray(String(o.side ?? ""));
140
+ const coin = String(o.symbol ?? "");
141
+ const d = adapter.describeCoin(coin);
142
+ const strip = (n) => n.replace(/^template:/, "");
143
+ return [
144
+ d !== null ? `#${d.outcome} ${strip(d.name)} · ${strip(d.sideName)} (side ${d.side}, ${coin})` : coin,
145
+ sideLabel,
146
+ String(o.size ?? ""),
147
+ String(o.price ?? ""),
148
+ String(o.orderId ?? ""),
149
+ ];
150
+ });
151
+ console.log("\n" + makeTable(["outcome · side", "dir", "size", "price", "oid"], rows) + "\n");
152
+ });
153
+ // ── outcome buy / sell ───────────────────────────────────────────────────
154
+ for (const action of ["buy", "sell"]) {
155
+ const cmd = supportsDryRun(outcome.command(`${action} <outcome> <side> [usd]`))
156
+ .description(`${action === "buy" ? "Buy" : "Sell"} a side of an outcome: [usd] notional in the outcome's quote token (outcomeMeta quoteToken, e.g. USDC), or --size <shares>${action === "sell" ? " / --all (the whole position)" : ""}; supports --dry-run`)
157
+ .option("--limit <px>", "Limit price (default: aggressive Ioc at top of book)")
158
+ .option("--tif <tif>", "Time-in-force: gtc | ioc | alo (default: ioc for market, gtc for limit)")
159
+ .option("--size <shares>", "Number of shares (integer) instead of a notional");
160
+ if (action === "sell")
161
+ cmd.option("--all", "Sell the whole position of this outcome side (shares read from the account)");
162
+ cmd
163
+ .action(async (outcomeArg, sideArg, usdArg, opts, command) => {
164
+ // --dry-run is the root flag (never re-declared here — the root would swallow it).
165
+ const merged = { ...opts, dryRun: isDryRun(command) };
166
+ const given = [usdArg !== undefined, opts.size !== undefined, opts.all === true].filter(Boolean).length;
167
+ if (given !== 1) {
168
+ throw new PerpError("INVALID_PARAMS", `outcome ${action}: give exactly one of <usd>, --size <shares>${action === "sell" ? ", --all" : ""}`, {
169
+ remediation: `e.g. perp outcome ${action} <outcome> <side> 12 | --size 13${action === "sell" ? " | --all" : ""}`,
170
+ });
171
+ }
172
+ const adapter = await getOutcomeAdapter();
173
+ const { outcome: outcomeId, side } = await resolveOutcomeSide(adapter, outcomeArg, sideArg);
174
+ const usd = usdArg !== undefined ? Number(usdArg) : null;
175
+ if (usd !== null && (!Number.isFinite(usd) || usd <= 0)) {
176
+ throw new PerpError("INVALID_PARAMS", `Invalid notional: ${usdArg}`, {});
177
+ }
178
+ if (opts.size !== undefined && !/^[1-9]\d*$/.test(opts.size)) {
179
+ throw new PerpError("INVALID_PARAMS", `--size must be a positive whole number of shares (got ${opts.size})`, {});
180
+ }
181
+ // Shares held on this side now (spot outcome balance); every path reads it for the remainder check.
182
+ const held = Number((await adapter.getPositions()).find(p => p.outcome === outcomeId && p.side === side)?.size ?? "0");
183
+ if (opts.all === true && !(Math.floor(held) >= 1)) {
184
+ throw new PerpError("POSITION_NOT_FOUND", `No whole share of outcome ${outcomeId} side ${side} to sell (held ${held})`, { remediation: "See: perp outcome positions" });
185
+ }
186
+ // Resolve price + size from notional
187
+ let price = merged.limit;
188
+ const tif = (merged.tif?.toUpperCase() ?? (price ? "GTC" : "IOC"));
189
+ if (!price) {
190
+ const book = await adapter.getOrderbook(outcomeId, side);
191
+ if (action === "buy") {
192
+ const bestAsk = book.asks[0]?.[0];
193
+ if (!bestAsk)
194
+ throw new PerpError("EXCHANGE_ERROR", `No asks available for outcome ${outcomeId} side ${side}`, {});
195
+ // Aggressive Ioc: pay 5% above best ask, capped at $1
196
+ price = Math.min(1, Number(bestAsk) * 1.05).toFixed(5);
197
+ }
198
+ else {
199
+ const bestBid = book.bids[0]?.[0];
200
+ if (!bestBid)
201
+ throw new PerpError("EXCHANGE_ERROR", `No bids available for outcome ${outcomeId} side ${side}`, {});
202
+ // Aggressive Ioc: sell 5% below best bid, floored at $0
203
+ price = Math.max(0.0001, Number(bestBid) * 0.95).toFixed(5);
204
+ }
205
+ }
206
+ // Outcome sizes are integer shares.
207
+ const size = opts.all === true ? String(Math.floor(held)) : opts.size !== undefined ? opts.size : String(Math.floor(usd / Number(price)));
208
+ if (action === "sell" && Number(size) > held) {
209
+ throw new PerpError("SIZE_TOO_LARGE", `outcome sell ${size} shares exceeds the ${held} held on outcome ${outcomeId} side ${side}; nothing was sent`, { remediation: "Use --all, or a smaller --size" });
210
+ }
211
+ const tifMap = { GTC: "Gtc", IOC: "Ioc", ALO: "Alo" };
212
+ const tifNorm = tifMap[tif];
213
+ const orderInfo = {
214
+ outcome: outcomeId,
215
+ side,
216
+ isBuy: action === "buy",
217
+ price,
218
+ size,
219
+ tif: tifNorm,
220
+ notional: (Number(price) * Number(size)).toFixed(4),
221
+ };
222
+ // Validate min-notional client-side so --dry-run surfaces it too
223
+ // (placeOrder enforces but is bypassed by --dry-run early return).
224
+ const quote = adapter.quoteTokenOf(outcomeId);
225
+ // A position worth less than the 10-quote minimum can never be sold (every order must be >= 10):
226
+ // warn before an order that would leave such a remainder.
227
+ const after = action === "buy" ? held + Number(size) : held - Number(size);
228
+ if (after > 0 && after * Number(price) < 10) {
229
+ const msg = `this ${action} leaves ${after} share(s) ≈ ${(after * Number(price)).toFixed(2)} ${quote} on outcome ${outcomeId} side ${side} — below the 10 ${quote} minimum order value, so that remainder cannot be sold on its own${action === "sell" ? " (use --all to sell everything)" : ""}`;
230
+ process.stderr.write(chalk.yellow(` WARNING: ${msg}\n`));
231
+ }
232
+ if (Number(orderInfo.notional) < 10) {
233
+ throw new PerpError("INVALID_PARAMS", `Outcome order value must be at least 10 ${quote} (got price=${price} * size=${size} = ${orderInfo.notional})`, { exchange: "hyperliquid", remediation: "Increase --usd or pass --limit so price*size >= 10" });
234
+ }
235
+ if (merged.dryRun) {
236
+ if (isJson())
237
+ return printJson(jsonOk({ dryRun: true, sent: false, order: orderInfo }));
238
+ console.log(chalk.yellow(`\n [dry-run] ${action.toUpperCase()} outcome=${outcomeId} side=${side} ${size}@${price} (notional ~${orderInfo.notional} ${quote}, tif=${tifNorm})\n`));
239
+ return;
240
+ }
241
+ const result = await adapter.placeOrder({
242
+ outcome: outcomeId,
243
+ side,
244
+ isBuy: action === "buy",
245
+ price,
246
+ size,
247
+ tif: tifNorm,
248
+ });
249
+ if (isJson())
250
+ return printJson(jsonOk({ order: orderInfo, response: result }));
251
+ console.log(chalk.green(`\n ${action.toUpperCase()} placed: outcome=${outcomeId} side=${side} ${size}@${price}\n`));
252
+ console.log(` ${chalk.gray("response:")} ${JSON.stringify(result)}\n`);
253
+ });
254
+ }
255
+ // ── outcome cancel <outcome> <side> <oid> ────────────────────────────────
256
+ outcome
257
+ .command("cancel <outcome> <side> <oid>")
258
+ .description("Cancel an open outcome order")
259
+ .action(async (outcomeArg, sideArg, oidArg) => {
260
+ const adapter = await getOutcomeAdapter();
261
+ const { outcome: outcomeId, side } = await resolveOutcomeSide(adapter, outcomeArg, sideArg);
262
+ const oid = Number(oidArg);
263
+ if (!Number.isInteger(oid) || oid <= 0) {
264
+ throw new PerpError("INVALID_PARAMS", `Invalid oid: ${oidArg}`, {});
265
+ }
266
+ const result = await adapter.cancelOrder(outcomeId, side, oid);
267
+ if (isJson())
268
+ return printJson(jsonOk(result));
269
+ console.log(chalk.green(`\n Cancelled order ${oid} on outcome=${outcomeId} side=${side}\n`));
270
+ });
271
+ }
272
+ // ── Helpers ────────────────────────────────────────────────────────────────
273
+ async function resolveOutcomeSide(adapter, outcomeArg, sideArg) {
274
+ const outcomeId = Number(outcomeArg);
275
+ if (!Number.isInteger(outcomeId) || outcomeId < 0) {
276
+ throw new PerpError("INVALID_PARAMS", `Invalid outcome id: ${outcomeArg}`, {});
277
+ }
278
+ // side may be: integer (0/1), name (Yes/No), or `#<enc>`/`+<enc>` where
279
+ // enc encodes BOTH outcome and side. The encoded form is verified against
280
+ // outcomeArg — silently picking the side digit while ignoring the outcome
281
+ // would route the trade to the wrong market.
282
+ let side;
283
+ if (/^\d+$/.test(sideArg)) {
284
+ side = Number(sideArg);
285
+ }
286
+ else if (/^[#+]\d+$/.test(sideArg)) {
287
+ const enc = Number(sideArg.slice(1));
288
+ const encodedOutcome = Math.floor(enc / 10);
289
+ const encodedSide = enc % 10;
290
+ if (encodedOutcome !== outcomeId) {
291
+ throw new PerpError("INVALID_PARAMS", `Side reference '${sideArg}' encodes outcome=${encodedOutcome} but the outcome arg is ${outcomeId}. ` +
292
+ `Either use plain side (0/1/Yes/No) or pass outcome=${encodedOutcome}.`, {});
293
+ }
294
+ side = encodedSide;
295
+ }
296
+ else {
297
+ const markets = await adapter.getMarkets();
298
+ const market = markets.find((m) => m.outcome === outcomeId);
299
+ const match = market?.sides.find((s) => s.name.toLowerCase() === sideArg.toLowerCase());
300
+ side = match?.side;
301
+ }
302
+ if (side === undefined || !Number.isInteger(side)) {
303
+ throw new PerpError("INVALID_PARAMS", `Invalid side: ${sideArg} (use 0/1, Yes/No, or #<enc>)`, {});
304
+ }
305
+ return { outcome: outcomeId, side };
306
+ }
307
+ function printOutcomeView(view) {
308
+ console.log(chalk.white.bold(`\n Outcome #${view.outcome} — ${view.name}`));
309
+ console.log(chalk.gray(` ${view.description}`));
310
+ // Header line: target / underlying mark / gap / expiry
311
+ if (view.underlying) {
312
+ const u = view.underlying;
313
+ const target = u.targetPrice !== undefined ? `$${u.targetPrice.toLocaleString()}` : "—";
314
+ const mark = u.markPrice !== undefined ? `$${Number(u.markPrice).toLocaleString()}` : chalk.gray("—");
315
+ const gapStr = u.gap !== undefined && u.gapPct !== undefined
316
+ ? (u.gap >= 0
317
+ ? chalk.green(`+$${Math.abs(u.gap).toFixed(2)} (+${u.gapPct.toFixed(2)}%)`)
318
+ : chalk.red(`-$${Math.abs(u.gap).toFixed(2)} (${u.gapPct.toFixed(2)}%)`))
319
+ : chalk.gray("—");
320
+ const itm = u.inTheMoney === "yes" ? chalk.green("Yes ITM")
321
+ : u.inTheMoney === "no" ? chalk.red("No ITM")
322
+ : chalk.gray("—");
323
+ console.log(` ${u.symbol} target ${target} current ${mark} gap ${gapStr} ${itm}`);
324
+ }
325
+ if (view.expiryMs !== undefined) {
326
+ const expiryStr = new Date(view.expiryMs).toISOString().replace("T", " ").slice(0, 16) + " UTC";
327
+ const ttx = view.msToExpiry !== undefined ? formatDuration(view.msToExpiry) : "—";
328
+ console.log(` Expires: ${expiryStr} (${ttx})`);
329
+ }
330
+ if (view.midSum !== undefined) {
331
+ const sumColor = Math.abs(view.midSum - 1) < 0.01 ? chalk.gray : chalk.yellow;
332
+ console.log(` Implied probabilities (sum ${sumColor(view.midSum.toFixed(4))}):`);
333
+ for (const s of view.sides) {
334
+ const p = s.impliedProb !== undefined ? `${(s.impliedProb * 100).toFixed(1)}%` : "—";
335
+ console.log(` ${s.name.padEnd(6)} ${p}`);
336
+ }
337
+ }
338
+ // One table per side. Stacked layout — readable on any terminal width
339
+ // and avoids the alignment quirks that come from padding ANSI strings.
340
+ console.log("");
341
+ for (const s of view.sides) {
342
+ console.log(` ${chalk.white.bold(s.name)} book`);
343
+ const maxLevels = Math.max(s.bids.length, s.asks.length);
344
+ const rows = [];
345
+ for (let i = 0; i < maxLevels; i++) {
346
+ const b = s.bids[i];
347
+ const a = s.asks[i];
348
+ rows.push([
349
+ b ? chalk.green(b[0]) : "",
350
+ b ? b[1] : "",
351
+ a ? chalk.red(a[0]) : "",
352
+ a ? a[1] : "",
353
+ ]);
354
+ }
355
+ console.log(makeTable(["bid", "size", "ask", "size"], rows));
356
+ console.log("");
357
+ }
358
+ }
359
+ function formatDuration(ms) {
360
+ if (ms < 0)
361
+ return "expired";
362
+ const s = Math.floor(ms / 1000);
363
+ const h = Math.floor(s / 3600);
364
+ const m = Math.floor((s % 3600) / 60);
365
+ if (h > 24)
366
+ return `${Math.floor(h / 24)}d ${h % 24}h ${m}m left`;
367
+ if (h > 0)
368
+ return `${h}h ${m}m left`;
369
+ if (m > 0)
370
+ return `${m}m ${s % 60}s left`;
371
+ return `${s}s left`;
372
+ }
373
+ function printOutcomeBook(book, depth) {
374
+ console.log(chalk.white.bold(`\n outcome=${book.outcome} side=${book.side}`));
375
+ const bids = book.bids.slice(0, depth);
376
+ const asks = book.asks.slice(0, depth);
377
+ const rows = [];
378
+ const max = Math.max(bids.length, asks.length);
379
+ for (let i = 0; i < max; i++) {
380
+ const b = bids[i];
381
+ const a = asks[i];
382
+ rows.push([
383
+ b ? `${chalk.green(b[0])} (${b[1]})` : "",
384
+ a ? `${chalk.red(a[0])} (${a[1]})` : "",
385
+ ]);
386
+ }
387
+ console.log("\n" + makeTable(["bid (size)", "ask (size)"], rows) + "\n");
388
+ }
389
+ function formatPnlCell(pnl) {
390
+ if (pnl === undefined)
391
+ return chalk.gray("—");
392
+ const n = Number(pnl);
393
+ return n >= 0
394
+ ? chalk.green(`+$${formatUsd(String(n.toFixed(4)))}`)
395
+ : chalk.red(`-$${formatUsd(String(Math.abs(n).toFixed(4)))}`);
396
+ }
@@ -1,7 +1,30 @@
1
+ import { isDryRun, supportsDryRun } from "../cli-flags.js";
1
2
  import { readFileSync } from "fs";
2
3
  import { printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
3
4
  import { validatePlan, executePlan } from "../plan-executor.js";
5
+ import { PerpError, extractErrorMessage } from "../errors.js";
4
6
  import chalk from "chalk";
7
+ /** Read and JSON-parse a plan file (`-` = stdin). A missing / unreadable / malformed file is an input error. */
8
+ function readPlanFile(file) {
9
+ const source = file === "-" ? "stdin" : `'${file}'`;
10
+ let raw;
11
+ try {
12
+ raw = file === "-" ? readFileSync(0, "utf-8") : readFileSync(file, "utf-8");
13
+ }
14
+ catch (err) {
15
+ throw new PerpError("INVALID_PARAMS", `Cannot read plan file ${source}: ${extractErrorMessage(err)}`, {
16
+ remediation: "Check the path (or pass - to read the plan from stdin). Generate a template with: perp strategy plan example",
17
+ });
18
+ }
19
+ try {
20
+ return JSON.parse(raw);
21
+ }
22
+ catch (err) {
23
+ throw new PerpError("INVALID_PARAMS", `Plan file ${source} is not valid JSON: ${extractErrorMessage(err)}`, {
24
+ remediation: "Fix the JSON. Generate a template with: perp strategy plan example",
25
+ });
26
+ }
27
+ }
5
28
  export function registerStrategyPlanCommands(parent, getAdapter, isJson) {
6
29
  const plan = parent.command("plan").description("Composite execution plans (multi-step atomic operations)");
7
30
  // ── plan validate ──
@@ -10,8 +33,7 @@ export function registerStrategyPlanCommands(parent, getAdapter, isJson) {
10
33
  .description("Validate a JSON execution plan without executing")
11
34
  .action(async (file) => {
12
35
  await withJsonErrors(isJson(), async () => {
13
- const raw = file === "-" ? readFileSync(0, "utf-8") : readFileSync(file, "utf-8");
14
- const planData = JSON.parse(raw);
36
+ const planData = readPlanFile(file);
15
37
  const result = validatePlan(planData);
16
38
  if (isJson())
17
39
  return printJson(jsonOk(result));
@@ -28,17 +50,16 @@ export function registerStrategyPlanCommands(parent, getAdapter, isJson) {
28
50
  });
29
51
  });
30
52
  // ── plan execute ──
31
- plan
32
- .command("execute <file>")
33
- .description("Execute a JSON plan (use - for stdin)")
34
- .option("--dry-run", "Simulate without executing trades")
35
- .action(async (file, opts) => {
53
+ supportsDryRun(plan.command("execute <file>"))
54
+ .description("Execute a JSON plan (use - for stdin; supports --dry-run)")
55
+ .action(async (file, _opts, cmd) => {
56
+ const dryRun = isDryRun(cmd);
36
57
  await withJsonErrors(isJson(), async () => {
37
- const raw = file === "-" ? readFileSync(0, "utf-8") : readFileSync(file, "utf-8");
38
- const planData = JSON.parse(raw);
58
+ const planData = readPlanFile(file);
39
59
  // Validate first
40
60
  const validation = validatePlan(planData);
41
61
  if (!validation.valid) {
62
+ process.exitCode = 1;
42
63
  if (isJson())
43
64
  return printJson(jsonError("INVALID_PARAMS", `Plan validation failed: ${validation.errors.join("; ")}`));
44
65
  console.error(chalk.red(`\n Plan validation failed:`));
@@ -49,14 +70,29 @@ export function registerStrategyPlanCommands(parent, getAdapter, isJson) {
49
70
  const adapter = await getAdapter();
50
71
  const log = isJson() ? () => { } : (msg) => console.log(chalk.gray(` ${msg}`));
51
72
  if (!isJson()) {
52
- console.log(chalk.cyan.bold(`\n Executing plan (${planData.steps.length} steps)${opts.dryRun ? " [DRY RUN]" : ""}...\n`));
73
+ console.log(chalk.cyan.bold(`\n Executing plan (${planData.steps.length} steps)${dryRun ? " [DRY RUN]" : ""}...\n`));
53
74
  }
54
75
  const result = await executePlan(adapter, planData, {
55
- dryRun: opts.dryRun,
76
+ dryRun,
56
77
  log,
57
78
  });
58
- if (isJson())
79
+ // Unknown / quarantined / failed outcomes must not look like success to scripts.
80
+ const bad = result.status === "pending" || result.status === "quarantined" || result.status === "failed" || result.status === "partial";
81
+ const badCode = result.status === "pending" ? "ORDER_PENDING"
82
+ : result.status === "quarantined" ? "ORDER_PARTIAL"
83
+ : result.status === "partial" ? "ORDER_PARTIAL"
84
+ : "ORDER_REJECTED";
85
+ const badRemediation = result.status === "pending" || result.status === "quarantined"
86
+ ? "An order outcome is unknown or a symbol was quarantined — do NOT re-run the plan. Check positions, then: perp strategy quarantine"
87
+ : "Check the failed step and current positions before re-running";
88
+ if (isJson()) {
89
+ if (bad) {
90
+ printJson(jsonError(badCode, `Plan ended with status "${result.status}"`, { details: { result }, remediation: badRemediation }));
91
+ process.exitCode = 1;
92
+ return;
93
+ }
59
94
  return printJson(jsonOk(result));
95
+ }
60
96
  // Pretty print results
61
97
  for (const step of result.steps) {
62
98
  const icon = step.status === "success" ? chalk.green("✓")
@@ -72,6 +108,10 @@ export function registerStrategyPlanCommands(parent, getAdapter, isJson) {
72
108
  : result.status === "partial" ? chalk.yellow
73
109
  : chalk.red;
74
110
  console.log(`\n Status: ${statusColor(result.status)} | ${result.totalDurationMs}ms\n`);
111
+ if (bad) {
112
+ console.log(chalk.red(` ${badRemediation}\n`));
113
+ process.exitCode = 1;
114
+ }
75
115
  });
76
116
  });
77
117
  // ── plan example ──
@@ -16,4 +16,5 @@
16
16
  */
17
17
  import type { Command } from "commander";
18
18
  import type { ExchangeAdapter } from "../exchanges/index.js";
19
+ import "../exchanges/venues/index.js";
19
20
  export declare function registerPortfolioCommand(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean, version: string, getHLAdapterForDex?: (dex: string) => Promise<ExchangeAdapter>): void;