perp-cli 0.3.3

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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/__tests__/alert-logic.test.d.ts +1 -0
  4. package/dist/__tests__/alert-logic.test.js +107 -0
  5. package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
  6. package/dist/__tests__/arb-auto-3dex.test.js +397 -0
  7. package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
  8. package/dist/__tests__/arb-history-stats.test.js +176 -0
  9. package/dist/__tests__/arb-logic.test.d.ts +1 -0
  10. package/dist/__tests__/arb-logic.test.js +84 -0
  11. package/dist/__tests__/arb-manage.test.d.ts +1 -0
  12. package/dist/__tests__/arb-manage.test.js +253 -0
  13. package/dist/__tests__/arb-new-features.test.d.ts +1 -0
  14. package/dist/__tests__/arb-new-features.test.js +457 -0
  15. package/dist/__tests__/arb-sizing.test.d.ts +1 -0
  16. package/dist/__tests__/arb-sizing.test.js +48 -0
  17. package/dist/__tests__/arb-state.test.d.ts +1 -0
  18. package/dist/__tests__/arb-state.test.js +284 -0
  19. package/dist/__tests__/arb-userflow.test.d.ts +1 -0
  20. package/dist/__tests__/arb-userflow.test.js +945 -0
  21. package/dist/__tests__/arb-utils.test.d.ts +1 -0
  22. package/dist/__tests__/arb-utils.test.js +264 -0
  23. package/dist/__tests__/bot-conditions.test.d.ts +1 -0
  24. package/dist/__tests__/bot-conditions.test.js +341 -0
  25. package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
  26. package/dist/__tests__/client-id-tracker.test.js +137 -0
  27. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
  28. package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
  29. package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
  30. package/dist/__tests__/commands/order-intent.test.js +600 -0
  31. package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
  32. package/dist/__tests__/commands/trade-commands.test.js +821 -0
  33. package/dist/__tests__/config.test.d.ts +1 -0
  34. package/dist/__tests__/config.test.js +86 -0
  35. package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
  36. package/dist/__tests__/cross-chain-margin.test.js +287 -0
  37. package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
  38. package/dist/__tests__/dex-asset-map.test.js +191 -0
  39. package/dist/__tests__/errors.test.d.ts +1 -0
  40. package/dist/__tests__/errors.test.js +110 -0
  41. package/dist/__tests__/event-stream.test.d.ts +1 -0
  42. package/dist/__tests__/event-stream.test.js +276 -0
  43. package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
  44. package/dist/__tests__/exchanges/interface.test.js +132 -0
  45. package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
  46. package/dist/__tests__/exchanges/mock-adapter.js +137 -0
  47. package/dist/__tests__/execution-log.test.d.ts +1 -0
  48. package/dist/__tests__/execution-log.test.js +106 -0
  49. package/dist/__tests__/funding-calc.test.d.ts +1 -0
  50. package/dist/__tests__/funding-calc.test.js +71 -0
  51. package/dist/__tests__/funding-history.test.d.ts +1 -0
  52. package/dist/__tests__/funding-history.test.js +343 -0
  53. package/dist/__tests__/funding-rates.test.d.ts +1 -0
  54. package/dist/__tests__/funding-rates.test.js +342 -0
  55. package/dist/__tests__/funding.test.d.ts +1 -0
  56. package/dist/__tests__/funding.test.js +173 -0
  57. package/dist/__tests__/gap-logic.test.d.ts +1 -0
  58. package/dist/__tests__/gap-logic.test.js +43 -0
  59. package/dist/__tests__/hip3-dex.test.d.ts +1 -0
  60. package/dist/__tests__/hip3-dex.test.js +234 -0
  61. package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
  62. package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
  63. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
  64. package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
  65. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
  66. package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
  67. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
  68. package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
  69. package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
  70. package/dist/__tests__/integration/bridge.integration.test.js +309 -0
  71. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
  72. package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
  73. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
  74. package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
  75. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
  76. package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
  77. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
  78. package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
  79. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
  80. package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
  81. package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
  82. package/dist/__tests__/integration/lighter.integration.test.js +53 -0
  83. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
  84. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
  85. package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
  86. package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
  87. package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
  88. package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
  89. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
  90. package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
  91. package/dist/__tests__/liquidity.test.d.ts +1 -0
  92. package/dist/__tests__/liquidity.test.js +225 -0
  93. package/dist/__tests__/plan-executor.test.d.ts +1 -0
  94. package/dist/__tests__/plan-executor.test.js +314 -0
  95. package/dist/__tests__/position-history.test.d.ts +1 -0
  96. package/dist/__tests__/position-history.test.js +367 -0
  97. package/dist/__tests__/retry.test.d.ts +1 -0
  98. package/dist/__tests__/retry.test.js +310 -0
  99. package/dist/__tests__/risk-assessment.test.d.ts +1 -0
  100. package/dist/__tests__/risk-assessment.test.js +145 -0
  101. package/dist/__tests__/security-adversarial.test.d.ts +1 -0
  102. package/dist/__tests__/security-adversarial.test.js +574 -0
  103. package/dist/__tests__/strategies.test.d.ts +1 -0
  104. package/dist/__tests__/strategies.test.js +539 -0
  105. package/dist/__tests__/trade-execution.test.d.ts +1 -0
  106. package/dist/__tests__/trade-execution.test.js +129 -0
  107. package/dist/__tests__/trade-validator.test.d.ts +1 -0
  108. package/dist/__tests__/trade-validator.test.js +655 -0
  109. package/dist/__tests__/utils.test.d.ts +1 -0
  110. package/dist/__tests__/utils.test.js +76 -0
  111. package/dist/api/public/hyperliquid.d.ts +18 -0
  112. package/dist/api/public/hyperliquid.js +82 -0
  113. package/dist/api/public/index.d.ts +8 -0
  114. package/dist/api/public/index.js +8 -0
  115. package/dist/api/public/lighter.d.ts +24 -0
  116. package/dist/api/public/lighter.js +100 -0
  117. package/dist/api/public/pacifica.d.ts +17 -0
  118. package/dist/api/public/pacifica.js +54 -0
  119. package/dist/api/public/urls.d.ts +12 -0
  120. package/dist/api/public/urls.js +33 -0
  121. package/dist/arb/history-stats.d.ts +44 -0
  122. package/dist/arb/history-stats.js +135 -0
  123. package/dist/arb/index.d.ts +4 -0
  124. package/dist/arb/index.js +4 -0
  125. package/dist/arb/sizing.d.ts +23 -0
  126. package/dist/arb/sizing.js +96 -0
  127. package/dist/arb/state.d.ts +51 -0
  128. package/dist/arb/state.js +112 -0
  129. package/dist/arb/utils.d.ts +81 -0
  130. package/dist/arb/utils.js +267 -0
  131. package/dist/arb-history-stats.d.ts +5 -0
  132. package/dist/arb-history-stats.js +5 -0
  133. package/dist/arb-sizing.d.ts +5 -0
  134. package/dist/arb-sizing.js +5 -0
  135. package/dist/arb-state.d.ts +5 -0
  136. package/dist/arb-state.js +5 -0
  137. package/dist/arb-utils.d.ts +5 -0
  138. package/dist/arb-utils.js +5 -0
  139. package/dist/bot/conditions.d.ts +32 -0
  140. package/dist/bot/conditions.js +141 -0
  141. package/dist/bot/config.d.ts +76 -0
  142. package/dist/bot/config.js +160 -0
  143. package/dist/bot/engine.d.ts +8 -0
  144. package/dist/bot/engine.js +519 -0
  145. package/dist/bot/presets.d.ts +11 -0
  146. package/dist/bot/presets.js +296 -0
  147. package/dist/bridge-engine.d.ts +133 -0
  148. package/dist/bridge-engine.js +1487 -0
  149. package/dist/cache.d.ts +25 -0
  150. package/dist/cache.js +99 -0
  151. package/dist/cli-spec.d.ts +50 -0
  152. package/dist/cli-spec.js +75 -0
  153. package/dist/client-id-tracker.d.ts +25 -0
  154. package/dist/client-id-tracker.js +76 -0
  155. package/dist/commands/account.d.ts +3 -0
  156. package/dist/commands/account.js +425 -0
  157. package/dist/commands/agent.d.ts +3 -0
  158. package/dist/commands/agent.js +386 -0
  159. package/dist/commands/alert.d.ts +2 -0
  160. package/dist/commands/alert.js +421 -0
  161. package/dist/commands/analytics.d.ts +3 -0
  162. package/dist/commands/analytics.js +311 -0
  163. package/dist/commands/arb/index.d.ts +3 -0
  164. package/dist/commands/arb/index.js +921 -0
  165. package/dist/commands/arb-auto.d.ts +54 -0
  166. package/dist/commands/arb-auto.js +1328 -0
  167. package/dist/commands/arb-manage.d.ts +5 -0
  168. package/dist/commands/arb-manage.js +5 -0
  169. package/dist/commands/arb.d.ts +2 -0
  170. package/dist/commands/arb.js +347 -0
  171. package/dist/commands/backtest.d.ts +2 -0
  172. package/dist/commands/backtest.js +327 -0
  173. package/dist/commands/bot.d.ts +3 -0
  174. package/dist/commands/bot.js +412 -0
  175. package/dist/commands/bridge.d.ts +2 -0
  176. package/dist/commands/bridge.js +396 -0
  177. package/dist/commands/dashboard.d.ts +3 -0
  178. package/dist/commands/dashboard.js +176 -0
  179. package/dist/commands/deposit.d.ts +4 -0
  180. package/dist/commands/deposit.js +573 -0
  181. package/dist/commands/dex.d.ts +3 -0
  182. package/dist/commands/dex.js +114 -0
  183. package/dist/commands/env.d.ts +2 -0
  184. package/dist/commands/env.js +136 -0
  185. package/dist/commands/funding.d.ts +2 -0
  186. package/dist/commands/funding.js +347 -0
  187. package/dist/commands/gap.d.ts +2 -0
  188. package/dist/commands/gap.js +305 -0
  189. package/dist/commands/health.d.ts +2 -0
  190. package/dist/commands/health.js +67 -0
  191. package/dist/commands/history.d.ts +2 -0
  192. package/dist/commands/history.js +235 -0
  193. package/dist/commands/init.d.ts +15 -0
  194. package/dist/commands/init.js +266 -0
  195. package/dist/commands/jobs.d.ts +2 -0
  196. package/dist/commands/jobs.js +133 -0
  197. package/dist/commands/manage.d.ts +4 -0
  198. package/dist/commands/manage.js +309 -0
  199. package/dist/commands/market.d.ts +3 -0
  200. package/dist/commands/market.js +225 -0
  201. package/dist/commands/plan.d.ts +3 -0
  202. package/dist/commands/plan.js +95 -0
  203. package/dist/commands/portfolio.d.ts +3 -0
  204. package/dist/commands/portfolio.js +169 -0
  205. package/dist/commands/rebalance.d.ts +3 -0
  206. package/dist/commands/rebalance.js +293 -0
  207. package/dist/commands/risk.d.ts +3 -0
  208. package/dist/commands/risk.js +169 -0
  209. package/dist/commands/run.d.ts +3 -0
  210. package/dist/commands/run.js +202 -0
  211. package/dist/commands/settings.d.ts +2 -0
  212. package/dist/commands/settings.js +102 -0
  213. package/dist/commands/stream.d.ts +5 -0
  214. package/dist/commands/stream.js +123 -0
  215. package/dist/commands/trade.d.ts +3 -0
  216. package/dist/commands/trade.js +1273 -0
  217. package/dist/commands/wallet.d.ts +14 -0
  218. package/dist/commands/wallet.js +602 -0
  219. package/dist/commands/withdraw.d.ts +3 -0
  220. package/dist/commands/withdraw.js +187 -0
  221. package/dist/config.d.ts +5 -0
  222. package/dist/config.js +68 -0
  223. package/dist/cross-chain-margin.d.ts +46 -0
  224. package/dist/cross-chain-margin.js +107 -0
  225. package/dist/dashboard/server.d.ts +80 -0
  226. package/dist/dashboard/server.js +340 -0
  227. package/dist/dashboard/ui.d.ts +4 -0
  228. package/dist/dashboard/ui.js +538 -0
  229. package/dist/dashboard/ws-feeds.d.ts +29 -0
  230. package/dist/dashboard/ws-feeds.js +660 -0
  231. package/dist/dex-asset-map.d.ts +80 -0
  232. package/dist/dex-asset-map.js +201 -0
  233. package/dist/errors.d.ts +109 -0
  234. package/dist/errors.js +84 -0
  235. package/dist/event-stream.d.ts +25 -0
  236. package/dist/event-stream.js +168 -0
  237. package/dist/exchanges/hyperliquid.d.ts +212 -0
  238. package/dist/exchanges/hyperliquid.js +931 -0
  239. package/dist/exchanges/interface.d.ts +95 -0
  240. package/dist/exchanges/interface.js +5 -0
  241. package/dist/exchanges/lighter.d.ts +159 -0
  242. package/dist/exchanges/lighter.js +793 -0
  243. package/dist/exchanges/pacifica.d.ts +51 -0
  244. package/dist/exchanges/pacifica.js +248 -0
  245. package/dist/execution-log.d.ts +36 -0
  246. package/dist/execution-log.js +102 -0
  247. package/dist/funding/history.d.ts +63 -0
  248. package/dist/funding/history.js +266 -0
  249. package/dist/funding/index.d.ts +3 -0
  250. package/dist/funding/index.js +3 -0
  251. package/dist/funding/normalize.d.ts +39 -0
  252. package/dist/funding/normalize.js +66 -0
  253. package/dist/funding/rates.d.ts +45 -0
  254. package/dist/funding/rates.js +172 -0
  255. package/dist/funding-history.d.ts +5 -0
  256. package/dist/funding-history.js +5 -0
  257. package/dist/funding-rates.d.ts +5 -0
  258. package/dist/funding-rates.js +5 -0
  259. package/dist/funding.d.ts +5 -0
  260. package/dist/funding.js +5 -0
  261. package/dist/index.d.ts +2 -0
  262. package/dist/index.js +458 -0
  263. package/dist/jobs.d.ts +37 -0
  264. package/dist/jobs.js +152 -0
  265. package/dist/liquidity.d.ts +34 -0
  266. package/dist/liquidity.js +100 -0
  267. package/dist/mcp-server.d.ts +9 -0
  268. package/dist/mcp-server.js +1206 -0
  269. package/dist/pacifica/client.d.ts +111 -0
  270. package/dist/pacifica/client.js +310 -0
  271. package/dist/pacifica/constants.d.ts +27 -0
  272. package/dist/pacifica/constants.js +47 -0
  273. package/dist/pacifica/deposit.d.ts +14 -0
  274. package/dist/pacifica/deposit.js +78 -0
  275. package/dist/pacifica/index.d.ts +6 -0
  276. package/dist/pacifica/index.js +11 -0
  277. package/dist/pacifica/signing.d.ts +49 -0
  278. package/dist/pacifica/signing.js +97 -0
  279. package/dist/pacifica/types/account.d.ts +42 -0
  280. package/dist/pacifica/types/account.js +1 -0
  281. package/dist/pacifica/types/index.d.ts +6 -0
  282. package/dist/pacifica/types/index.js +6 -0
  283. package/dist/pacifica/types/lake.d.ts +18 -0
  284. package/dist/pacifica/types/lake.js +1 -0
  285. package/dist/pacifica/types/market.d.ts +64 -0
  286. package/dist/pacifica/types/market.js +1 -0
  287. package/dist/pacifica/types/order.d.ts +92 -0
  288. package/dist/pacifica/types/order.js +1 -0
  289. package/dist/pacifica/types/position.d.ts +25 -0
  290. package/dist/pacifica/types/position.js +1 -0
  291. package/dist/pacifica/types/ws.d.ts +34 -0
  292. package/dist/pacifica/types/ws.js +41 -0
  293. package/dist/pacifica/ws-client.d.ts +42 -0
  294. package/dist/pacifica/ws-client.js +180 -0
  295. package/dist/plan-executor.d.ts +48 -0
  296. package/dist/plan-executor.js +280 -0
  297. package/dist/position-history.d.ts +68 -0
  298. package/dist/position-history.js +222 -0
  299. package/dist/rebalance.d.ts +64 -0
  300. package/dist/rebalance.js +142 -0
  301. package/dist/retry.d.ts +74 -0
  302. package/dist/retry.js +129 -0
  303. package/dist/risk.d.ts +48 -0
  304. package/dist/risk.js +156 -0
  305. package/dist/settings.d.ts +19 -0
  306. package/dist/settings.js +45 -0
  307. package/dist/shared-api.d.ts +5 -0
  308. package/dist/shared-api.js +5 -0
  309. package/dist/strategies/dca.d.ts +25 -0
  310. package/dist/strategies/dca.js +114 -0
  311. package/dist/strategies/funding-arb.d.ts +15 -0
  312. package/dist/strategies/funding-arb.js +281 -0
  313. package/dist/strategies/grid.d.ts +34 -0
  314. package/dist/strategies/grid.js +185 -0
  315. package/dist/strategies/trailing-stop.d.ts +17 -0
  316. package/dist/strategies/trailing-stop.js +121 -0
  317. package/dist/strategies/twap.d.ts +20 -0
  318. package/dist/strategies/twap.js +78 -0
  319. package/dist/trade-validator.d.ts +39 -0
  320. package/dist/trade-validator.js +154 -0
  321. package/dist/utils.d.ts +38 -0
  322. package/dist/utils.js +110 -0
  323. package/package.json +63 -0
  324. package/skills/perp-cli/SKILL.md +149 -0
  325. package/skills/perp-cli/references/commands.md +143 -0
@@ -0,0 +1,1273 @@
1
+ import { PacificaAdapter } from "../exchanges/pacifica.js";
2
+ import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
3
+ import { LighterAdapter } from "../exchanges/lighter.js";
4
+ import { printJson, errorAndExit, withJsonErrors, jsonOk, jsonError, symbolMatch, formatUsd } from "../utils.js";
5
+ import { logExecution } from "../execution-log.js";
6
+ import { validateTrade } from "../trade-validator.js";
7
+ import { generateClientId, logClientId, isOrderDuplicate } from "../client-id-tracker.js";
8
+ import chalk from "chalk";
9
+ function pac(adapter) {
10
+ if (!(adapter instanceof PacificaAdapter))
11
+ throw new Error("This command requires --exchange pacifica");
12
+ return adapter;
13
+ }
14
+ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = () => false) {
15
+ /** Guard: if --dry-run is active, log the intended action and return without executing. */
16
+ function dryRunGuard(action, details) {
17
+ if (!isDryRun())
18
+ return false;
19
+ const info = { dryRun: true, action, ...details, timestamp: new Date().toISOString() };
20
+ if (isJson()) {
21
+ printJson(jsonOk(info));
22
+ }
23
+ else {
24
+ console.log(chalk.yellow(`\n [DRY RUN] Would ${action}:`));
25
+ for (const [k, v] of Object.entries(details)) {
26
+ console.log(chalk.gray(` ${k}: ${v}`));
27
+ }
28
+ console.log();
29
+ }
30
+ logExecution({
31
+ type: action.includes("limit") ? "limit_order" : action.includes("stop") ? "stop_order" : action.includes("cancel") ? "cancel" : "market_order",
32
+ exchange: details.exchange ?? "unknown",
33
+ symbol: (details.symbol ?? "").toUpperCase(),
34
+ side: details.side ?? "",
35
+ size: String(details.size ?? "0"),
36
+ price: details.price,
37
+ status: "simulated",
38
+ dryRun: true,
39
+ });
40
+ return true;
41
+ }
42
+ const trade = program.command("trade").description("Trading commands");
43
+ // === Generic commands (all exchanges) ===
44
+ trade
45
+ .command("market <symbol> <side> <size>")
46
+ .description("Place a market order (side: buy/sell)")
47
+ .option("-s, --slippage <pct>", "Slippage percent", "1")
48
+ .option("--reduce-only", "Reduce only order")
49
+ .option("--client-id <id>", "Client order ID for idempotent tracking")
50
+ .option("--auto-id", "Auto-generate a client order ID")
51
+ .action(async (symbol, side, size, opts) => {
52
+ const s = side.toLowerCase();
53
+ if (s !== "buy" && s !== "sell")
54
+ errorAndExit("Side must be buy or sell");
55
+ const clientId = opts.autoId ? generateClientId() : opts.clientId;
56
+ if (clientId && isOrderDuplicate(clientId)) {
57
+ if (isJson())
58
+ return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
59
+ console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
60
+ return;
61
+ }
62
+ const adapter = await getAdapter();
63
+ if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size }))
64
+ return;
65
+ if (clientId) {
66
+ logClientId({
67
+ clientOrderId: clientId, exchange: adapter.name,
68
+ symbol: symbol.toUpperCase(), side: s, size, type: "market",
69
+ status: "pending", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
70
+ });
71
+ }
72
+ let result;
73
+ try {
74
+ result = await adapter.marketOrder(symbol.toUpperCase(), s, size);
75
+ logExecution({
76
+ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
77
+ side: s, size, status: "success", dryRun: false,
78
+ meta: clientId ? { clientOrderId: clientId } : undefined,
79
+ });
80
+ }
81
+ catch (err) {
82
+ logExecution({
83
+ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
84
+ side: s, size, status: "failed", dryRun: false,
85
+ error: err instanceof Error ? err.message : String(err),
86
+ meta: clientId ? { clientOrderId: clientId } : undefined,
87
+ });
88
+ throw err;
89
+ }
90
+ if (clientId) {
91
+ logClientId({
92
+ clientOrderId: clientId, exchange: adapter.name,
93
+ symbol: symbol.toUpperCase(), side: s, size, type: "market",
94
+ status: "submitted", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
95
+ });
96
+ }
97
+ if (isJson())
98
+ return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
99
+ console.log(chalk.green(`\n Market ${s.toUpperCase()} ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.${clientId ? ` (id: ${clientId})` : ""}\n`));
100
+ printJson(jsonOk(result));
101
+ });
102
+ // Shortcuts: trade buy / trade sell
103
+ trade
104
+ .command("buy <symbol> <size>")
105
+ .description("Market buy (shortcut for: trade market <symbol> buy <size>)")
106
+ .option("-s, --slippage <pct>", "Slippage percent", "1")
107
+ .option("--reduce-only", "Reduce only order")
108
+ .option("--client-id <id>", "Client order ID")
109
+ .option("--auto-id", "Auto-generate client order ID")
110
+ .action(async (symbol, size, opts) => {
111
+ const clientId = opts.autoId ? generateClientId() : opts.clientId;
112
+ if (clientId && isOrderDuplicate(clientId)) {
113
+ if (isJson())
114
+ return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
115
+ console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
116
+ return;
117
+ }
118
+ const adapter = await getAdapter();
119
+ if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size }))
120
+ return;
121
+ let result;
122
+ try {
123
+ result = await adapter.marketOrder(symbol.toUpperCase(), "buy", size);
124
+ logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size, status: "success", dryRun: false });
125
+ }
126
+ catch (err) {
127
+ logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size, status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
128
+ throw err;
129
+ }
130
+ if (isJson())
131
+ return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
132
+ console.log(chalk.green(`\n Market BUY ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.\n`));
133
+ printJson(jsonOk(result));
134
+ });
135
+ trade
136
+ .command("sell <symbol> <size>")
137
+ .description("Market sell (shortcut for: trade market <symbol> sell <size>)")
138
+ .option("-s, --slippage <pct>", "Slippage percent", "1")
139
+ .option("--reduce-only", "Reduce only order")
140
+ .option("--client-id <id>", "Client order ID")
141
+ .option("--auto-id", "Auto-generate client order ID")
142
+ .action(async (symbol, size, opts) => {
143
+ const clientId = opts.autoId ? generateClientId() : opts.clientId;
144
+ if (clientId && isOrderDuplicate(clientId)) {
145
+ if (isJson())
146
+ return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
147
+ console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
148
+ return;
149
+ }
150
+ const adapter = await getAdapter();
151
+ if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size }))
152
+ return;
153
+ let result;
154
+ try {
155
+ result = await adapter.marketOrder(symbol.toUpperCase(), "sell", size);
156
+ logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size, status: "success", dryRun: false });
157
+ }
158
+ catch (err) {
159
+ logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size, status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
160
+ throw err;
161
+ }
162
+ if (isJson())
163
+ return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
164
+ console.log(chalk.green(`\n Market SELL ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.\n`));
165
+ printJson(jsonOk(result));
166
+ });
167
+ trade
168
+ .command("limit <symbol> <side> <price> <size>")
169
+ .description("Place a limit order")
170
+ .option("--tif <tif>", "Time in force: GTC, IOC, ALO, TOB", "GTC")
171
+ .option("--reduce-only", "Reduce only order")
172
+ .option("--client-id <id>", "Client order ID for idempotent tracking")
173
+ .option("--auto-id", "Auto-generate a client order ID")
174
+ .action(async (symbol, side, price, size, opts) => {
175
+ const s = side.toLowerCase();
176
+ if (s !== "buy" && s !== "sell")
177
+ errorAndExit("Side must be buy or sell");
178
+ const clientId = opts.autoId ? generateClientId() : opts.clientId;
179
+ if (clientId && isOrderDuplicate(clientId)) {
180
+ if (isJson())
181
+ return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
182
+ console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
183
+ return;
184
+ }
185
+ const adapter = await getAdapter();
186
+ if (dryRunGuard("limit_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price }))
187
+ return;
188
+ if (clientId) {
189
+ logClientId({
190
+ clientOrderId: clientId, exchange: adapter.name,
191
+ symbol: symbol.toUpperCase(), side: s, size, type: "limit",
192
+ status: "pending", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
193
+ });
194
+ }
195
+ let result;
196
+ try {
197
+ result = await adapter.limitOrder(symbol.toUpperCase(), s, price, size);
198
+ logExecution({
199
+ type: "limit_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
200
+ side: s, size, price, status: "success", dryRun: false,
201
+ meta: clientId ? { clientOrderId: clientId } : undefined,
202
+ });
203
+ }
204
+ catch (err) {
205
+ logExecution({
206
+ type: "limit_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
207
+ side: s, size, price, status: "failed", dryRun: false,
208
+ error: err instanceof Error ? err.message : String(err),
209
+ meta: clientId ? { clientOrderId: clientId } : undefined,
210
+ });
211
+ throw err;
212
+ }
213
+ if (clientId) {
214
+ logClientId({
215
+ clientOrderId: clientId, exchange: adapter.name,
216
+ symbol: symbol.toUpperCase(), side: s, size, type: "limit",
217
+ status: "submitted", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
218
+ });
219
+ }
220
+ if (isJson())
221
+ return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
222
+ console.log(chalk.green(`\n Limit ${s.toUpperCase()} ${size} ${symbol.toUpperCase()} @ $${price} placed on ${adapter.name}.${clientId ? ` (id: ${clientId})` : ""}\n`));
223
+ printJson(jsonOk(result));
224
+ });
225
+ trade
226
+ .command("cancel <symbol> <orderId>")
227
+ .description("Cancel a specific order")
228
+ .action(async (symbol, orderId) => {
229
+ const adapter = await getAdapter();
230
+ if (dryRunGuard("cancel", { exchange: adapter.name, symbol: symbol.toUpperCase(), orderId }))
231
+ return;
232
+ try {
233
+ const result = await adapter.cancelOrder(symbol.toUpperCase(), orderId);
234
+ logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId } });
235
+ if (isJson())
236
+ return printJson(jsonOk(result));
237
+ console.log(chalk.green(`\n Order ${orderId} cancelled on ${adapter.name}.\n`));
238
+ }
239
+ catch (err) {
240
+ logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId } });
241
+ throw err;
242
+ }
243
+ });
244
+ trade
245
+ .command("cancel-all")
246
+ .description("Cancel all open orders")
247
+ .action(async () => {
248
+ const adapter = await getAdapter();
249
+ if (dryRunGuard("cancel_all", { exchange: adapter.name }))
250
+ return;
251
+ const result = await adapter.cancelAllOrders();
252
+ if (isJson())
253
+ return printJson(jsonOk(result));
254
+ console.log(chalk.green(`\n All orders cancelled on ${adapter.name}.\n`));
255
+ });
256
+ // === TWAP — Pacifica (seconds) + Hyperliquid (minutes) ===
257
+ trade
258
+ .command("twap <symbol> <side> <size> <duration>")
259
+ .description("Place a TWAP order (Pacifica: seconds, HL: minutes, Lighter/any: client-side via --background)")
260
+ .option("-s, --slippage <pct>", "Slippage percent", "1")
261
+ .option("--reduce-only", "Reduce only order")
262
+ .option("--background", "Run client-side TWAP in background (tmux) — works on all exchanges")
263
+ .option("--slices <n>", "Number of slices for client-side TWAP")
264
+ .action(async (symbol, side, size, duration, opts) => {
265
+ const s = side.toLowerCase();
266
+ if (s !== "buy" && s !== "sell")
267
+ errorAndExit("Side must be buy or sell");
268
+ // --background → client-side TWAP via tmux job
269
+ if (opts.background) {
270
+ const { startJob } = await import("../jobs.js");
271
+ const exchange = (await getAdapter()).name;
272
+ const cliArgs = [
273
+ symbol.toUpperCase(), s, size, duration,
274
+ ...(opts.slices ? ["--slices", opts.slices] : []),
275
+ ];
276
+ // Pass exchange flag through
277
+ const job = startJob({
278
+ strategy: "twap",
279
+ exchange,
280
+ params: { symbol: symbol.toUpperCase(), side: s, size, duration, slices: opts.slices },
281
+ cliArgs: [`-e`, exchange, ...cliArgs],
282
+ });
283
+ if (isJson())
284
+ return printJson(jsonOk(job));
285
+ console.log(chalk.green(`\n TWAP job started in background.`));
286
+ console.log(` ID: ${chalk.white.bold(job.id)}`);
287
+ console.log(` Session: ${job.tmuxSession}`);
288
+ console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
289
+ console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
290
+ return;
291
+ }
292
+ const adapter = await getAdapter();
293
+ // Lighter or any exchange without native TWAP → client-side TWAP (foreground)
294
+ if (adapter instanceof LighterAdapter) {
295
+ const { runTWAP } = await import("../strategies/twap.js");
296
+ const result = await runTWAP(adapter, {
297
+ symbol: symbol.toUpperCase(),
298
+ side: s,
299
+ totalSize: parseFloat(size),
300
+ durationSec: parseInt(duration),
301
+ slices: opts.slices ? parseInt(opts.slices) : undefined,
302
+ });
303
+ if (isJson())
304
+ return printJson(jsonOk(result));
305
+ return;
306
+ }
307
+ let result;
308
+ if (adapter instanceof PacificaAdapter) {
309
+ result = await adapter.sdk.createTWAP({
310
+ symbol: symbol.toUpperCase(),
311
+ amount: size,
312
+ side: s === "buy" ? "bid" : "ask",
313
+ slippage_percent: opts.slippage,
314
+ reduce_only: opts.reduceOnly ?? false,
315
+ duration_in_seconds: parseInt(duration),
316
+ }, adapter.publicKey, adapter.signer);
317
+ }
318
+ else if (adapter instanceof HyperliquidAdapter) {
319
+ const minutes = parseInt(duration);
320
+ if (minutes < 5 || minutes > 1440)
321
+ errorAndExit("HL TWAP duration must be 5-1440 minutes");
322
+ result = await adapter.twapOrder(symbol.toUpperCase(), s, size, minutes, { reduceOnly: opts.reduceOnly });
323
+ }
324
+ else {
325
+ errorAndExit("TWAP orders require --exchange pacifica, hyperliquid, or lighter");
326
+ }
327
+ if (isJson())
328
+ return printJson(jsonOk(result));
329
+ console.log(chalk.green(`\n TWAP ${s.toUpperCase()} ${size} ${symbol.toUpperCase()} over ${duration} placed on ${adapter.name}.\n`));
330
+ printJson(jsonOk(result));
331
+ });
332
+ // === Stop / Trigger orders — Pacifica + Hyperliquid ===
333
+ trade
334
+ .command("stop <symbol> <side> <stopPrice> <size>")
335
+ .description("Place a stop order")
336
+ .option("--limit-price <price>", "Limit price (makes it stop-limit)")
337
+ .option("--reduce-only", "Reduce only order")
338
+ .action(async (symbol, side, stopPrice, size, opts) => {
339
+ const s = side.toLowerCase();
340
+ if (s !== "buy" && s !== "sell")
341
+ errorAndExit("Side must be buy or sell");
342
+ const adapter = await getAdapter();
343
+ if (dryRunGuard("stop_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price: stopPrice }))
344
+ return;
345
+ let result;
346
+ try {
347
+ result = await adapter.stopOrder(symbol.toUpperCase(), s, size, stopPrice, { limitPrice: opts.limitPrice, reduceOnly: opts.reduceOnly });
348
+ logExecution({ type: "stop_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price: stopPrice, status: "success", dryRun: false });
349
+ }
350
+ catch (err) {
351
+ logExecution({ type: "stop_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price: stopPrice, status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
352
+ throw err;
353
+ }
354
+ if (isJson())
355
+ return printJson(jsonOk(result));
356
+ console.log(chalk.green(`\n Stop order placed on ${adapter.name}.\n`));
357
+ printJson(jsonOk(result));
358
+ });
359
+ // === TP/SL — Pacifica + Hyperliquid ===
360
+ trade
361
+ .command("tpsl <symbol> <side>")
362
+ .description("Set take-profit / stop-loss on a position")
363
+ .option("--tp <price>", "Take profit trigger price")
364
+ .option("--tp-limit <price>", "Take profit limit price")
365
+ .option("--sl <price>", "Stop loss trigger price")
366
+ .option("--size <size>", "Size (HL only, omit for full position)")
367
+ .action(async (symbol, side, opts) => {
368
+ const s = side.toLowerCase();
369
+ if (s !== "buy" && s !== "sell")
370
+ errorAndExit("Side must be buy or sell");
371
+ if (!opts.tp && !opts.sl)
372
+ errorAndExit("Must specify --tp and/or --sl");
373
+ const adapter = await getAdapter();
374
+ if (dryRunGuard("tpsl", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, tp: opts.tp ?? "none", sl: opts.sl ?? "none" }))
375
+ return;
376
+ if (adapter instanceof PacificaAdapter) {
377
+ // TP/SL side is opposite of position: LONG position → "ask" to close
378
+ const params = {
379
+ symbol: symbol.toUpperCase(),
380
+ side: s === "buy" ? "ask" : "bid",
381
+ };
382
+ if (opts.tp)
383
+ params.take_profit = { stop_price: opts.tp, limit_price: opts.tpLimit };
384
+ if (opts.sl)
385
+ params.stop_loss = { stop_price: opts.sl };
386
+ const result = await adapter.sdk.setTPSL(params, adapter.publicKey, adapter.signer);
387
+ if (isJson())
388
+ return printJson(jsonOk(result));
389
+ console.log(chalk.green(`\n TP/SL set for ${symbol.toUpperCase()} on Pacifica.\n`));
390
+ }
391
+ else if (adapter instanceof HyperliquidAdapter) {
392
+ // HL uses trigger orders for TP/SL
393
+ const results = [];
394
+ const posSize = opts.size || "0"; // 0 = full position via positionTpsl grouping
395
+ if (opts.tp) {
396
+ results.push(await adapter.triggerOrder(symbol.toUpperCase(), s === "buy" ? "sell" : "buy", // Close opposite side
397
+ posSize, opts.tp, "tp", { isMarket: !opts.tpLimit, reduceOnly: true, grouping: "positionTpsl" }));
398
+ }
399
+ if (opts.sl) {
400
+ results.push(await adapter.triggerOrder(symbol.toUpperCase(), s === "buy" ? "sell" : "buy", posSize, opts.sl, "sl", { isMarket: true, reduceOnly: true, grouping: "positionTpsl" }));
401
+ }
402
+ if (isJson())
403
+ return printJson(jsonOk(results));
404
+ console.log(chalk.green(`\n TP/SL set for ${symbol.toUpperCase()} on Hyperliquid.\n`));
405
+ }
406
+ else if (adapter instanceof LighterAdapter) {
407
+ // Lighter uses triggerPrice in signCreateOrder for TP/SL
408
+ const closeSide = s === "buy" ? "sell" : "buy"; // Close opposite side
409
+ const results = [];
410
+ if (opts.tp) {
411
+ results.push(await adapter.stopOrder(symbol.toUpperCase(), closeSide, opts.size || "0", opts.tp, { limitPrice: opts.tpLimit, reduceOnly: true }));
412
+ }
413
+ if (opts.sl) {
414
+ results.push(await adapter.stopOrder(symbol.toUpperCase(), closeSide, opts.size || "0", opts.sl, { reduceOnly: true }));
415
+ }
416
+ if (isJson())
417
+ return printJson(jsonOk(results));
418
+ console.log(chalk.green(`\n TP/SL set for ${symbol.toUpperCase()} on Lighter.\n`));
419
+ }
420
+ else {
421
+ errorAndExit("TP/SL requires --exchange pacifica, hyperliquid, or lighter");
422
+ }
423
+ });
424
+ // === Scaled Take-Profit (분할익절) ===
425
+ trade
426
+ .command("scale-tp <symbol>")
427
+ .description("Place multiple take-profit limit orders at different price levels (분할익절)")
428
+ .requiredOption("--levels <levels>", "Comma-separated price:percent pairs (e.g., 72000:25,75000:25,80000:50)")
429
+ .option("--size <size>", "Override total position size (default: uses current position)")
430
+ .action(async (symbol, opts) => {
431
+ const sym = symbol.toUpperCase();
432
+ const adapter = await getAdapter();
433
+ // Parse levels: "72000:25,75000:25,80000:50"
434
+ const levels = opts.levels.split(",").map(l => {
435
+ const [price, pct] = l.trim().split(":");
436
+ if (!price || !pct)
437
+ errorAndExit(`Invalid level format: ${l}. Use price:percent (e.g., 72000:25)`);
438
+ return { price: price.trim(), pct: parseFloat(pct.trim()) };
439
+ });
440
+ // Validate percentages sum to 100
441
+ const totalPct = levels.reduce((s, l) => s + l.pct, 0);
442
+ if (Math.abs(totalPct - 100) > 0.01) {
443
+ errorAndExit(`Percentages must sum to 100%. Got: ${totalPct}%`);
444
+ }
445
+ // Get current position to determine size and side
446
+ let totalSize;
447
+ let closeSide;
448
+ if (opts.size) {
449
+ totalSize = parseFloat(opts.size);
450
+ // Need to know position side — fetch it
451
+ const positions = await adapter.getPositions();
452
+ const pos = positions.find(p => p.symbol.toUpperCase() === sym);
453
+ closeSide = pos?.side === "short" ? "buy" : "sell";
454
+ }
455
+ else {
456
+ const positions = await adapter.getPositions();
457
+ const pos = positions.find(p => p.symbol.toUpperCase() === sym);
458
+ if (!pos)
459
+ errorAndExit(`No open position for ${sym}. Use --size to specify manually.`);
460
+ totalSize = parseFloat(pos.size);
461
+ closeSide = pos.side === "long" ? "sell" : "buy";
462
+ }
463
+ if (dryRunGuard("scale_tp", {
464
+ exchange: adapter.name, symbol: sym, side: closeSide,
465
+ totalSize: totalSize.toString(),
466
+ levels: levels.map(l => `${l.price}@${l.pct}%`).join(", "),
467
+ }))
468
+ return;
469
+ // Place reduce-only limit orders at each level
470
+ const results = [];
471
+ for (const level of levels) {
472
+ const levelSize = (totalSize * level.pct / 100).toString();
473
+ try {
474
+ const result = await adapter.limitOrder(sym, closeSide, level.price, levelSize, { reduceOnly: true });
475
+ results.push({ price: level.price, size: levelSize, pct: level.pct, result });
476
+ logExecution({
477
+ type: "limit_order", exchange: adapter.name, symbol: sym,
478
+ side: closeSide, size: levelSize, price: level.price,
479
+ status: "success", dryRun: false,
480
+ meta: { action: "scale-tp", pct: level.pct, reduceOnly: true },
481
+ });
482
+ }
483
+ catch (err) {
484
+ const msg = err instanceof Error ? err.message : String(err);
485
+ logExecution({
486
+ type: "limit_order", exchange: adapter.name, symbol: sym,
487
+ side: closeSide, size: levelSize, price: level.price,
488
+ status: "failed", dryRun: false, error: msg,
489
+ meta: { action: "scale-tp", pct: level.pct },
490
+ });
491
+ if (isJson()) {
492
+ results.push({ price: level.price, size: levelSize, pct: level.pct, result: { error: msg } });
493
+ }
494
+ else {
495
+ console.log(chalk.red(` Failed: ${level.price} x ${levelSize} — ${msg}`));
496
+ }
497
+ }
498
+ }
499
+ if (isJson())
500
+ return printJson(jsonOk({ symbol: sym, side: closeSide, totalSize, levels: results }));
501
+ console.log(chalk.green(`\n Scaled TP set for ${sym} on ${adapter.name}:\n`));
502
+ for (const r of results) {
503
+ const status = r.result?.error ? chalk.red("FAILED") : chalk.green("OK");
504
+ console.log(` ${status} $${r.price} — ${r.pct}% (${r.size} ${sym})`);
505
+ }
506
+ console.log();
507
+ });
508
+ // === Pacifica-only commands ===
509
+ trade
510
+ .command("edit <symbol> <orderId> <price> <size>")
511
+ .description("Edit an existing order")
512
+ .action(async (symbol, orderId, price, size) => {
513
+ const adapter = await getAdapter();
514
+ if (dryRunGuard("edit_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), orderId, price, size }))
515
+ return;
516
+ const result = await adapter.editOrder(symbol.toUpperCase(), orderId, price, size);
517
+ if (isJson())
518
+ return printJson(jsonOk(result));
519
+ console.log(chalk.green(`\n Order ${orderId} updated to $${price} x ${size} on ${adapter.name}.\n`));
520
+ });
521
+ trade
522
+ .command("cancel-stop <symbol> <stopOrderId>")
523
+ .description("Cancel a stop order (Pacifica)")
524
+ .action(async (symbol, stopOrderId) => {
525
+ const adapter = await getAdapter();
526
+ const p = pac(adapter);
527
+ const result = await p.sdk.cancelStopOrder({ symbol: symbol.toUpperCase(), order_id: Number(stopOrderId) }, p.publicKey, p.signer);
528
+ if (isJson())
529
+ return printJson(jsonOk(result));
530
+ console.log(chalk.green(`\n Stop order ${stopOrderId} cancelled.\n`));
531
+ });
532
+ trade
533
+ .command("cancel-twap <symbol> <twapOrderId>")
534
+ .description("Cancel a TWAP order")
535
+ .action(async (symbol, twapOrderId) => {
536
+ const adapter = await getAdapter();
537
+ let result;
538
+ if (adapter instanceof PacificaAdapter) {
539
+ result = await adapter.sdk.cancelTWAP({ symbol: symbol.toUpperCase(), twap_order_id: Number(twapOrderId) }, adapter.publicKey, adapter.signer);
540
+ }
541
+ else if (adapter instanceof HyperliquidAdapter) {
542
+ result = await adapter.twapCancel(symbol.toUpperCase(), Number(twapOrderId));
543
+ }
544
+ else {
545
+ errorAndExit("Cancel TWAP requires --exchange pacifica or hyperliquid");
546
+ }
547
+ if (isJson())
548
+ return printJson(jsonOk(result));
549
+ console.log(chalk.green(`\n TWAP order ${twapOrderId} cancelled on ${adapter.name}.\n`));
550
+ });
551
+ // === Hyperliquid-only commands ===
552
+ trade
553
+ .command("leverage <symbol> <leverage>")
554
+ .description("Set leverage for a symbol")
555
+ .option("--isolated", "Use isolated margin mode (default: cross)")
556
+ .action(async (symbol, leverage, opts) => {
557
+ const adapter = await getAdapter();
558
+ const mode = opts.isolated ? "isolated" : "cross";
559
+ if (dryRunGuard("set_leverage", { exchange: adapter.name, symbol: symbol.toUpperCase(), leverage, mode }))
560
+ return;
561
+ try {
562
+ const result = await adapter.setLeverage(symbol.toUpperCase(), parseInt(leverage), mode);
563
+ logExecution({
564
+ type: "rebalance", exchange: adapter.name, symbol: symbol.toUpperCase(), side: mode,
565
+ size: leverage, status: "success", dryRun: false,
566
+ meta: { action: "set_leverage", leverage: parseInt(leverage), mode },
567
+ });
568
+ if (isJson())
569
+ return printJson(jsonOk(result));
570
+ console.log(chalk.green(`\n Leverage set to ${leverage}x (${mode}) for ${symbol.toUpperCase()} on ${adapter.name}.\n`));
571
+ }
572
+ catch (err) {
573
+ logExecution({
574
+ type: "rebalance", exchange: adapter.name, symbol: symbol.toUpperCase(), side: mode,
575
+ size: leverage, status: "failed", dryRun: false,
576
+ error: err instanceof Error ? err.message : String(err),
577
+ meta: { action: "set_leverage", leverage: parseInt(leverage), mode },
578
+ });
579
+ throw err;
580
+ }
581
+ });
582
+ // ── Grid Bot (shortcut with --background) ──
583
+ trade
584
+ .command("grid <symbol>")
585
+ .description("Run grid trading bot (foreground or --background)")
586
+ .requiredOption("--upper <price>", "Upper price bound")
587
+ .requiredOption("--lower <price>", "Lower price bound")
588
+ .option("--grids <n>", "Number of grid lines", "10")
589
+ .option("--size <size>", "Total position size (base)", "0.1")
590
+ .option("--side <side>", "Grid bias: long, short, neutral", "neutral")
591
+ .option("--leverage <n>", "Leverage to set")
592
+ .option("--interval <sec>", "Check interval in seconds", "10")
593
+ .option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
594
+ .option("--trailing-stop <pct>", "Stop if equity drops by this % from peak")
595
+ .option("--background", "Run in background (tmux)")
596
+ .action(async (symbol, opts) => {
597
+ const exchange = (await getAdapter()).name;
598
+ const cliArgs = [
599
+ `-e`, exchange, symbol.toUpperCase(),
600
+ `--upper`, opts.upper, `--lower`, opts.lower,
601
+ `--grids`, opts.grids, `--size`, opts.size,
602
+ `--side`, opts.side, `--interval`, opts.interval,
603
+ `--max-runtime`, opts.maxRuntime,
604
+ ...(opts.leverage ? [`--leverage`, opts.leverage] : []),
605
+ ...(opts.trailingStop ? [`--trailing-stop`, opts.trailingStop] : []),
606
+ ];
607
+ if (opts.background) {
608
+ const { startJob } = await import("../jobs.js");
609
+ const job = startJob({
610
+ strategy: "grid",
611
+ exchange,
612
+ params: { symbol: symbol.toUpperCase(), ...opts },
613
+ cliArgs,
614
+ });
615
+ if (isJson())
616
+ return printJson(jsonOk(job));
617
+ console.log(chalk.green(`\n Grid bot started in background.`));
618
+ console.log(` ID: ${chalk.white.bold(job.id)}`);
619
+ console.log(` Range: $${opts.lower} - $${opts.upper} | ${opts.grids} grids`);
620
+ console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
621
+ console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
622
+ return;
623
+ }
624
+ // Foreground: delegate to `run grid`
625
+ const { runGrid } = await import("../strategies/grid.js");
626
+ const adapter = await getAdapter();
627
+ const log = (msg) => {
628
+ const ts = new Date().toLocaleTimeString();
629
+ console.log(`${chalk.gray(ts)} ${msg}`);
630
+ };
631
+ await runGrid(adapter, {
632
+ symbol: symbol.toUpperCase(),
633
+ side: opts.side,
634
+ upperPrice: parseFloat(opts.upper),
635
+ lowerPrice: parseFloat(opts.lower),
636
+ grids: parseInt(opts.grids),
637
+ totalSize: parseFloat(opts.size),
638
+ leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
639
+ intervalSec: parseInt(opts.interval),
640
+ maxRuntime: parseInt(opts.maxRuntime),
641
+ trailingStop: opts.trailingStop ? parseFloat(opts.trailingStop) : undefined,
642
+ }, undefined, log);
643
+ });
644
+ // ── DCA (shortcut with --background) ──
645
+ trade
646
+ .command("dca <symbol> <side> <amount> <interval>")
647
+ .description("Run DCA strategy (foreground or --background)")
648
+ .option("--orders <n>", "Total number of orders (0 = unlimited)", "0")
649
+ .option("--price-limit <price>", "Stop buying above / selling below this price")
650
+ .option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
651
+ .option("--background", "Run in background (tmux)")
652
+ .action(async (symbol, side, amount, interval, opts) => {
653
+ const s = side.toLowerCase();
654
+ if (s !== "buy" && s !== "sell")
655
+ errorAndExit("Side must be buy or sell");
656
+ const exchange = (await getAdapter()).name;
657
+ const cliArgs = [
658
+ `-e`, exchange, symbol.toUpperCase(), s, amount, interval,
659
+ `--orders`, opts.orders, `--max-runtime`, opts.maxRuntime,
660
+ ...(opts.priceLimit ? [`--price-limit`, opts.priceLimit] : []),
661
+ ];
662
+ if (opts.background) {
663
+ const { startJob } = await import("../jobs.js");
664
+ const job = startJob({
665
+ strategy: "dca",
666
+ exchange,
667
+ params: { symbol: symbol.toUpperCase(), side: s, amount, interval, ...opts },
668
+ cliArgs,
669
+ });
670
+ if (isJson())
671
+ return printJson(jsonOk(job));
672
+ console.log(chalk.green(`\n DCA started in background.`));
673
+ console.log(` ID: ${chalk.white.bold(job.id)}`);
674
+ console.log(` ${s.toUpperCase()} ${amount} ${symbol.toUpperCase()} every ${interval}s`);
675
+ console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
676
+ console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
677
+ return;
678
+ }
679
+ // Foreground
680
+ const { runDCA } = await import("../strategies/dca.js");
681
+ const adapter = await getAdapter();
682
+ const log = (msg) => {
683
+ const ts = new Date().toLocaleTimeString();
684
+ console.log(`${chalk.gray(ts)} ${msg}`);
685
+ };
686
+ await runDCA(adapter, {
687
+ symbol: symbol.toUpperCase(),
688
+ side: s,
689
+ amountPerOrder: parseFloat(amount),
690
+ intervalSec: parseInt(interval),
691
+ totalOrders: parseInt(opts.orders),
692
+ priceLimit: opts.priceLimit ? parseFloat(opts.priceLimit) : undefined,
693
+ maxRuntime: parseInt(opts.maxRuntime),
694
+ }, undefined, log);
695
+ });
696
+ // ── Position Management Shortcuts ──
697
+ trade
698
+ .command("close-all")
699
+ .description("Close all open positions (market orders on opposite side)")
700
+ .action(async () => {
701
+ await withJsonErrors(isJson(), async () => {
702
+ const adapter = await getAdapter();
703
+ if (dryRunGuard("close_all", { exchange: adapter.name }))
704
+ return;
705
+ const positions = await adapter.getPositions();
706
+ if (positions.length === 0) {
707
+ if (isJson())
708
+ return printJson(jsonOk({ closed: 0, positions: [] }));
709
+ console.log(chalk.yellow("\n No open positions to close.\n"));
710
+ return;
711
+ }
712
+ if (!isJson())
713
+ console.log(chalk.cyan(`\n Closing ${positions.length} position(s) on ${adapter.name}...\n`));
714
+ const results = [];
715
+ for (const pos of positions) {
716
+ const closeSide = pos.side === "long" ? "sell" : "buy";
717
+ if (!isJson())
718
+ console.log(chalk.gray(` ${closeSide.toUpperCase()} ${pos.size} ${pos.symbol} (closing ${pos.side})...`));
719
+ const result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
720
+ results.push(result);
721
+ logExecution({
722
+ type: "market_order", exchange: adapter.name, symbol: pos.symbol,
723
+ side: closeSide, size: pos.size, status: "success", dryRun: false,
724
+ meta: { action: "close-all", originalSide: pos.side },
725
+ });
726
+ }
727
+ if (isJson())
728
+ return printJson(jsonOk({ closed: results.length, results }));
729
+ console.log(chalk.green(`\n Closed ${results.length} position(s) on ${adapter.name}.\n`));
730
+ });
731
+ });
732
+ trade
733
+ .command("close <symbol>")
734
+ .description("Close a specific symbol's position")
735
+ .action(async (symbol) => {
736
+ await withJsonErrors(isJson(), async () => {
737
+ const sym = symbol.toUpperCase();
738
+ const adapter = await getAdapter();
739
+ const positions = await adapter.getPositions();
740
+ const pos = positions.find(p => symbolMatch(p.symbol, sym));
741
+ if (!pos) {
742
+ if (isJson())
743
+ return printJson(jsonOk({ closed: false, reason: "no_position" }));
744
+ errorAndExit(`No open position for ${sym}`);
745
+ }
746
+ const closeSide = pos.side === "long" ? "sell" : "buy";
747
+ if (dryRunGuard("close", { exchange: adapter.name, symbol: sym, side: closeSide, size: pos.size, originalSide: pos.side }))
748
+ return;
749
+ if (!isJson())
750
+ console.log(chalk.cyan(`\n Closing ${pos.side} ${pos.size} ${sym} on ${adapter.name}...\n`));
751
+ const result = await adapter.marketOrder(sym, closeSide, pos.size);
752
+ logExecution({
753
+ type: "market_order", exchange: adapter.name, symbol: sym,
754
+ side: closeSide, size: pos.size, status: "success", dryRun: false,
755
+ meta: { action: "close", originalSide: pos.side },
756
+ });
757
+ if (isJson())
758
+ return printJson(jsonOk(result));
759
+ console.log(chalk.green(`\n Closed ${pos.side} ${pos.size} ${sym} on ${adapter.name}.\n`));
760
+ });
761
+ });
762
+ trade
763
+ .command("flatten")
764
+ .description("Cancel all orders AND close all positions (full cleanup)")
765
+ .action(async () => {
766
+ await withJsonErrors(isJson(), async () => {
767
+ const adapter = await getAdapter();
768
+ if (dryRunGuard("flatten", { exchange: adapter.name }))
769
+ return;
770
+ if (!isJson())
771
+ console.log(chalk.cyan(`\n Flattening account on ${adapter.name}...\n`));
772
+ // Step 1: Cancel all orders
773
+ if (!isJson())
774
+ console.log(chalk.gray(" Cancelling all open orders..."));
775
+ const cancelResult = await adapter.cancelAllOrders();
776
+ // Step 2: Close all positions
777
+ const positions = await adapter.getPositions();
778
+ const closeResults = [];
779
+ for (const pos of positions) {
780
+ const closeSide = pos.side === "long" ? "sell" : "buy";
781
+ if (!isJson())
782
+ console.log(chalk.gray(` ${closeSide.toUpperCase()} ${pos.size} ${pos.symbol} (closing ${pos.side})...`));
783
+ const result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
784
+ closeResults.push(result);
785
+ logExecution({
786
+ type: "market_order", exchange: adapter.name, symbol: pos.symbol,
787
+ side: closeSide, size: pos.size, status: "success", dryRun: false,
788
+ meta: { action: "flatten", originalSide: pos.side },
789
+ });
790
+ }
791
+ if (isJson())
792
+ return printJson(jsonOk({ ordersCancelled: cancelResult, positionsClosed: closeResults.length, closeResults }));
793
+ console.log(chalk.green(`\n Flattened: cancelled orders + closed ${closeResults.length} position(s) on ${adapter.name}.\n`));
794
+ });
795
+ });
796
+ trade
797
+ .command("reduce <symbol> <percent>")
798
+ .description("Reduce a position by a percentage (e.g., perp trade reduce BTC 50)")
799
+ .action(async (symbol, percent) => {
800
+ await withJsonErrors(isJson(), async () => {
801
+ const sym = symbol.toUpperCase();
802
+ const pct = parseFloat(percent);
803
+ if (isNaN(pct) || pct <= 0 || pct > 100)
804
+ errorAndExit("Percent must be between 0 and 100");
805
+ const adapter = await getAdapter();
806
+ const positions = await adapter.getPositions();
807
+ const pos = positions.find(p => symbolMatch(p.symbol, sym));
808
+ if (!pos) {
809
+ if (isJson())
810
+ return printJson(jsonOk({ reduced: false, reason: "no_position" }));
811
+ errorAndExit(`No open position for ${sym}`);
812
+ }
813
+ const fullSize = parseFloat(pos.size);
814
+ const reduceSize = (fullSize * pct / 100).toString();
815
+ const closeSide = pos.side === "long" ? "sell" : "buy";
816
+ if (dryRunGuard("reduce", { exchange: adapter.name, symbol: sym, side: closeSide, size: reduceSize, percent: pct, originalSize: pos.size }))
817
+ return;
818
+ if (!isJson())
819
+ console.log(chalk.cyan(`\n Reducing ${sym} ${pos.side} by ${pct}% (${reduceSize} of ${pos.size}) on ${adapter.name}...\n`));
820
+ const result = await adapter.marketOrder(sym, closeSide, reduceSize);
821
+ logExecution({
822
+ type: "market_order", exchange: adapter.name, symbol: sym,
823
+ side: closeSide, size: reduceSize, status: "success", dryRun: false,
824
+ meta: { action: "reduce", percent: pct, originalSize: pos.size, originalSide: pos.side },
825
+ });
826
+ if (isJson())
827
+ return printJson(jsonOk({ reduced: true, percent: pct, sizeReduced: reduceSize, originalSize: pos.size, result }));
828
+ console.log(chalk.green(`\n Reduced ${sym} by ${pct}% (${closeSide} ${reduceSize}) on ${adapter.name}.\n`));
829
+ });
830
+ });
831
+ // ── Order Status Query ──
832
+ trade
833
+ .command("status <orderId>")
834
+ .description("Query order status by ID")
835
+ .action(async (orderId) => {
836
+ await withJsonErrors(isJson(), async () => {
837
+ const adapter = await getAdapter();
838
+ if (adapter instanceof HyperliquidAdapter) {
839
+ const result = await adapter.queryOrder(Number(orderId));
840
+ if (isJson())
841
+ return printJson(jsonOk(result));
842
+ const order = result?.order;
843
+ if (!order) {
844
+ console.log(chalk.gray(`\n Order ${orderId} not found.\n`));
845
+ return;
846
+ }
847
+ const o = (order.order ?? order);
848
+ console.log(chalk.cyan.bold(`\n Order ${orderId}\n`));
849
+ console.log(` Symbol: ${o.coin ?? o.symbol ?? ""}`);
850
+ console.log(` Side: ${o.side === "B" ? chalk.green("BUY") : chalk.red("SELL")}`);
851
+ console.log(` Price: $${formatUsd(String(o.limitPx ?? o.price ?? "0"))}`);
852
+ console.log(` Size: ${o.sz ?? o.size ?? ""}`);
853
+ console.log(` Status: ${order.status ?? o.status ?? "unknown"}`);
854
+ console.log();
855
+ return;
856
+ }
857
+ // Generic: search order history
858
+ const [openOrders, history] = await Promise.all([
859
+ adapter.getOpenOrders(),
860
+ adapter.getOrderHistory(100),
861
+ ]);
862
+ const found = [...openOrders, ...history].find(o => o.orderId === orderId);
863
+ if (!found) {
864
+ if (isJson())
865
+ return printJson(jsonError("ORDER_NOT_FOUND", `Order ${orderId} not found`));
866
+ console.log(chalk.gray(`\n Order ${orderId} not found.\n`));
867
+ return;
868
+ }
869
+ if (isJson())
870
+ return printJson(jsonOk(found));
871
+ console.log(chalk.cyan.bold(`\n Order ${orderId}\n`));
872
+ console.log(` Symbol: ${found.symbol}`);
873
+ console.log(` Side: ${found.side === "buy" ? chalk.green("BUY") : chalk.red("SELL")}`);
874
+ console.log(` Type: ${found.type}`);
875
+ console.log(` Price: $${formatUsd(found.price)}`);
876
+ console.log(` Size: ${found.size}`);
877
+ console.log(` Filled: ${found.filled}`);
878
+ console.log(` Status: ${found.status}`);
879
+ console.log();
880
+ });
881
+ });
882
+ // ── Recent Fills ──
883
+ trade
884
+ .command("fills [symbol]")
885
+ .description("Recent trade fills, optionally filtered by symbol")
886
+ .option("-l, --limit <n>", "Number of fills", "30")
887
+ .action(async (symbol, opts) => {
888
+ await withJsonErrors(isJson(), async () => {
889
+ const adapter = await getAdapter();
890
+ const limit = parseInt(opts.limit);
891
+ const trades = await adapter.getTradeHistory(limit);
892
+ let filtered = trades;
893
+ if (symbol) {
894
+ const sym = symbol.toUpperCase();
895
+ filtered = trades.filter(t => symbolMatch(t.symbol, sym));
896
+ }
897
+ if (isJson())
898
+ return printJson(jsonOk(filtered));
899
+ if (filtered.length === 0) {
900
+ console.log(chalk.gray(`\n No fills${symbol ? ` for ${symbol.toUpperCase()}` : ""}.\n`));
901
+ return;
902
+ }
903
+ const rows = filtered.map((t) => [
904
+ new Date(t.time).toLocaleString(),
905
+ chalk.white.bold(t.symbol),
906
+ t.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
907
+ `$${formatUsd(t.price)}`,
908
+ t.size,
909
+ `$${formatUsd(t.fee)}`,
910
+ ]);
911
+ console.log((await import("../utils.js")).makeTable(["Time", "Symbol", "Side", "Price", "Size", "Fee"], rows));
912
+ });
913
+ });
914
+ // ── Pre-Trade Validation ──
915
+ trade
916
+ .command("check <symbol> <side> <size>")
917
+ .description("Validate a trade before execution (pre-flight check)")
918
+ .option("--price <price>", "Price for limit orders")
919
+ .option("--type <type>", "Order type: market, limit, stop", "market")
920
+ .option("--leverage <n>", "Leverage to use")
921
+ .option("--reduce-only", "Check as reduce-only order")
922
+ .action(async (symbol, side, size, opts) => {
923
+ await withJsonErrors(isJson(), async () => {
924
+ const s = side.toLowerCase();
925
+ if (s !== "buy" && s !== "sell")
926
+ errorAndExit("Side must be buy or sell");
927
+ const adapter = await getAdapter();
928
+ const validation = await validateTrade(adapter, {
929
+ symbol: symbol.toUpperCase(),
930
+ side: s,
931
+ size: parseFloat(size),
932
+ price: opts.price ? parseFloat(opts.price) : undefined,
933
+ type: (opts.type ?? "market"),
934
+ leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
935
+ reduceOnly: opts.reduceOnly,
936
+ });
937
+ if (isJson())
938
+ return printJson(jsonOk(validation));
939
+ console.log(chalk.cyan.bold(`\n Pre-Trade Check: ${symbol.toUpperCase()} ${s.toUpperCase()} ${size}\n`));
940
+ for (const check of validation.checks) {
941
+ const icon = check.passed ? chalk.green("✓") : chalk.red("✗");
942
+ console.log(` ${icon} ${check.check}: ${check.message}`);
943
+ }
944
+ if (validation.warnings.length > 0) {
945
+ console.log(chalk.yellow("\n Warnings:"));
946
+ for (const w of validation.warnings) {
947
+ console.log(` ⚠ ${w}`);
948
+ }
949
+ }
950
+ if (validation.estimatedCost) {
951
+ const c = validation.estimatedCost;
952
+ console.log(chalk.white.bold("\n Estimated Cost:"));
953
+ console.log(` Margin: $${c.margin.toFixed(2)}`);
954
+ console.log(` Fee: $${c.fee.toFixed(2)}`);
955
+ console.log(` Slippage: $${c.slippage.toFixed(2)}`);
956
+ console.log(` Total: $${c.total.toFixed(2)}`);
957
+ }
958
+ const resultColor = validation.valid ? chalk.green : chalk.red;
959
+ console.log(`\n Result: ${resultColor(validation.valid ? "VALID — safe to execute" : "INVALID — do not execute")}\n`);
960
+ });
961
+ });
962
+ // ── Scale-In (분할매수) ──
963
+ trade
964
+ .command("scale-in <symbol> <side>")
965
+ .description("Place multiple limit orders at different price levels to build a position gradually (분할매수)")
966
+ .requiredOption("--levels <levels>", "Comma-separated price:percent pairs (e.g., 65000:30,63000:30,60000:40)")
967
+ .option("--size-usd <usd>", "Total USD amount to deploy across all levels")
968
+ .option("--size <base>", "Total base amount (e.g., 0.01 BTC)")
969
+ .action(async (symbol, side, opts) => {
970
+ const sym = symbol.toUpperCase();
971
+ const s = side.toLowerCase();
972
+ if (s !== "buy" && s !== "sell")
973
+ errorAndExit("Side must be buy or sell");
974
+ if (!opts.sizeUsd && !opts.size)
975
+ errorAndExit("Must specify --size-usd or --size");
976
+ // Parse levels: "65000:30,63000:30,60000:40"
977
+ const levels = opts.levels.split(",").map(l => {
978
+ const [price, pct] = l.trim().split(":");
979
+ if (!price || !pct)
980
+ errorAndExit(`Invalid level format: ${l}. Use price:percent (e.g., 65000:30)`);
981
+ return { price: price.trim(), pct: parseFloat(pct.trim()) };
982
+ });
983
+ // Validate percentages sum to 100
984
+ const totalPct = levels.reduce((sum, l) => sum + l.pct, 0);
985
+ if (Math.abs(totalPct - 100) > 0.01) {
986
+ errorAndExit(`Percentages must sum to 100%. Got: ${totalPct}%`);
987
+ }
988
+ const adapter = await getAdapter();
989
+ // Compute sizes for each level
990
+ let levelSizes;
991
+ if (opts.sizeUsd) {
992
+ const totalUsd = parseFloat(opts.sizeUsd);
993
+ levelSizes = levels.map(l => ({
994
+ price: l.price,
995
+ pct: l.pct,
996
+ size: (totalUsd * l.pct / 100 / parseFloat(l.price)).toString(),
997
+ }));
998
+ }
999
+ else {
1000
+ const totalBase = parseFloat(opts.size);
1001
+ levelSizes = levels.map(l => ({
1002
+ price: l.price,
1003
+ pct: l.pct,
1004
+ size: (totalBase * l.pct / 100).toString(),
1005
+ }));
1006
+ }
1007
+ if (dryRunGuard("scale_in", {
1008
+ exchange: adapter.name, symbol: sym, side: s,
1009
+ totalSizeUsd: opts.sizeUsd ?? "N/A",
1010
+ totalSizeBase: opts.size ?? "N/A",
1011
+ levels: levelSizes.map(l => `${l.price}@${l.pct}% (${l.size})`).join(", "),
1012
+ }))
1013
+ return;
1014
+ // Place limit orders at each level (NOT reduce-only — opening positions)
1015
+ const results = [];
1016
+ for (const level of levelSizes) {
1017
+ try {
1018
+ const result = await adapter.limitOrder(sym, s, level.price, level.size);
1019
+ results.push({ price: level.price, size: level.size, pct: level.pct, result });
1020
+ logExecution({
1021
+ type: "limit_order", exchange: adapter.name, symbol: sym,
1022
+ side: s, size: level.size, price: level.price,
1023
+ status: "success", dryRun: false,
1024
+ meta: { action: "scale-in", pct: level.pct },
1025
+ });
1026
+ }
1027
+ catch (err) {
1028
+ const msg = err instanceof Error ? err.message : String(err);
1029
+ logExecution({
1030
+ type: "limit_order", exchange: adapter.name, symbol: sym,
1031
+ side: s, size: level.size, price: level.price,
1032
+ status: "failed", dryRun: false, error: msg,
1033
+ meta: { action: "scale-in", pct: level.pct },
1034
+ });
1035
+ if (isJson()) {
1036
+ results.push({ price: level.price, size: level.size, pct: level.pct, result: { error: msg } });
1037
+ }
1038
+ else {
1039
+ console.log(chalk.red(` Failed: ${level.price} x ${level.size} — ${msg}`));
1040
+ }
1041
+ }
1042
+ }
1043
+ if (isJson())
1044
+ return printJson(jsonOk({ symbol: sym, side: s, levels: results }));
1045
+ console.log(chalk.green(`\n Scale-in orders placed for ${sym} on ${adapter.name}:\n`));
1046
+ for (const r of results) {
1047
+ const status = r.result?.error ? chalk.red("FAILED") : chalk.green("OK");
1048
+ console.log(` ${status} $${r.price} — ${r.pct}% (${r.size} ${sym})`);
1049
+ }
1050
+ console.log();
1051
+ });
1052
+ // ── Trailing Stop ──
1053
+ trade
1054
+ .command("trailing-stop <symbol>")
1055
+ .description("Client-side trailing stop that monitors price and closes position when price drops by X% from peak")
1056
+ .requiredOption("--trail <pct>", "Trail percentage (e.g., 3 = close when price drops 3% from high)")
1057
+ .option("--interval <sec>", "Check interval in seconds", "5")
1058
+ .option("--activation <price>", "Only start trailing after price reaches this level")
1059
+ .option("--background", "Run in background (tmux)")
1060
+ .action(async (symbol, opts) => {
1061
+ const sym = symbol.toUpperCase();
1062
+ const trailPct = parseFloat(opts.trail);
1063
+ const intervalSec = parseInt(opts.interval);
1064
+ const activationPrice = opts.activation ? parseFloat(opts.activation) : undefined;
1065
+ if (isNaN(trailPct) || trailPct <= 0)
1066
+ errorAndExit("Trail percentage must be > 0");
1067
+ const exchange = (await getAdapter()).name;
1068
+ // --background → run via tmux
1069
+ if (opts.background) {
1070
+ const { startJob } = await import("../jobs.js");
1071
+ const cliArgs = [
1072
+ `-e`, exchange, sym,
1073
+ `--trail`, opts.trail,
1074
+ `--interval`, opts.interval,
1075
+ ...(opts.activation ? [`--activation`, opts.activation] : []),
1076
+ ];
1077
+ const job = startJob({
1078
+ strategy: "trailing-stop",
1079
+ exchange,
1080
+ params: { symbol: sym, trail: trailPct, interval: intervalSec, activation: activationPrice },
1081
+ cliArgs,
1082
+ });
1083
+ if (isJson())
1084
+ return printJson(jsonOk(job));
1085
+ console.log(chalk.green(`\n Trailing stop started in background.`));
1086
+ console.log(` ID: ${chalk.white.bold(job.id)}`);
1087
+ console.log(` Trail: ${trailPct}%${activationPrice ? ` | Activation: $${activationPrice}` : ""}`);
1088
+ console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
1089
+ console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
1090
+ return;
1091
+ }
1092
+ // Foreground: run trailing stop loop
1093
+ const adapter = await getAdapter();
1094
+ // Auto-detect position side
1095
+ const positions = await adapter.getPositions();
1096
+ const pos = positions.find(p => symbolMatch(p.symbol, sym));
1097
+ if (!pos)
1098
+ errorAndExit(`No open position for ${sym}. Open a position first.`);
1099
+ const positionSide = pos.side; // "long" or "short"
1100
+ const closeSide = positionSide === "long" ? "sell" : "buy";
1101
+ const posSize = pos.size;
1102
+ console.log(chalk.cyan(`\n Trailing Stop for ${sym} (${positionSide} ${posSize})`));
1103
+ console.log(chalk.cyan(` Trail: ${trailPct}% | Interval: ${intervalSec}s${activationPrice ? ` | Activation: $${activationPrice}` : ""}`));
1104
+ console.log(chalk.gray(` Press Ctrl+C to cancel.\n`));
1105
+ let peakPrice = 0;
1106
+ let activated = !activationPrice; // if no activation price, start immediately
1107
+ let running = true;
1108
+ const cleanup = () => { running = false; };
1109
+ process.on("SIGINT", cleanup);
1110
+ process.on("SIGTERM", cleanup);
1111
+ try {
1112
+ while (running) {
1113
+ const markets = await adapter.getMarkets();
1114
+ const market = markets.find(m => symbolMatch(m.symbol, sym));
1115
+ if (!market) {
1116
+ console.log(chalk.yellow(` Market data for ${sym} not found, retrying...`));
1117
+ await new Promise(r => setTimeout(r, intervalSec * 1000));
1118
+ continue;
1119
+ }
1120
+ const currentPrice = parseFloat(market.markPrice);
1121
+ // Check activation
1122
+ if (!activated && activationPrice) {
1123
+ if (positionSide === "long" && currentPrice >= activationPrice) {
1124
+ activated = true;
1125
+ console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (>= $${activationPrice})`));
1126
+ }
1127
+ else if (positionSide === "short" && currentPrice <= activationPrice) {
1128
+ activated = true;
1129
+ console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (<= $${activationPrice})`));
1130
+ }
1131
+ else {
1132
+ const ts = new Date().toLocaleTimeString();
1133
+ console.log(chalk.gray(` ${ts} | $${currentPrice.toFixed(2)} | Waiting for activation ($${activationPrice})...`));
1134
+ await new Promise(r => setTimeout(r, intervalSec * 1000));
1135
+ continue;
1136
+ }
1137
+ }
1138
+ // Update peak
1139
+ if (positionSide === "long") {
1140
+ if (currentPrice > peakPrice)
1141
+ peakPrice = currentPrice;
1142
+ const dropPct = ((peakPrice - currentPrice) / peakPrice) * 100;
1143
+ const ts = new Date().toLocaleTimeString();
1144
+ console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Peak: $${peakPrice.toFixed(2)} | Drop: ${dropPct.toFixed(2)}%`));
1145
+ if (dropPct >= trailPct) {
1146
+ console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price dropped ${dropPct.toFixed(2)}% from peak $${peakPrice.toFixed(2)}`));
1147
+ console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
1148
+ const result = await adapter.marketOrder(sym, closeSide, posSize);
1149
+ logExecution({
1150
+ type: "market_order", exchange: adapter.name, symbol: sym,
1151
+ side: closeSide, size: posSize, status: "success", dryRun: false,
1152
+ meta: { action: "trailing-stop", trailPct, peakPrice, triggerPrice: currentPrice },
1153
+ });
1154
+ if (isJson())
1155
+ return printJson(jsonOk({ triggered: true, peakPrice, triggerPrice: currentPrice, dropPct, result }));
1156
+ console.log(chalk.green(` Position closed. Peak: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
1157
+ return;
1158
+ }
1159
+ }
1160
+ else {
1161
+ // Short position: track lowest price, trigger when price rises
1162
+ if (peakPrice === 0 || currentPrice < peakPrice)
1163
+ peakPrice = currentPrice;
1164
+ const risePct = ((currentPrice - peakPrice) / peakPrice) * 100;
1165
+ const ts = new Date().toLocaleTimeString();
1166
+ console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Trough: $${peakPrice.toFixed(2)} | Rise: ${risePct.toFixed(2)}%`));
1167
+ if (risePct >= trailPct) {
1168
+ console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price rose ${risePct.toFixed(2)}% from trough $${peakPrice.toFixed(2)}`));
1169
+ console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
1170
+ const result = await adapter.marketOrder(sym, closeSide, posSize);
1171
+ logExecution({
1172
+ type: "market_order", exchange: adapter.name, symbol: sym,
1173
+ side: closeSide, size: posSize, status: "success", dryRun: false,
1174
+ meta: { action: "trailing-stop", trailPct, troughPrice: peakPrice, triggerPrice: currentPrice },
1175
+ });
1176
+ if (isJson())
1177
+ return printJson(jsonOk({ triggered: true, troughPrice: peakPrice, triggerPrice: currentPrice, risePct, result }));
1178
+ console.log(chalk.green(` Position closed. Trough: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
1179
+ return;
1180
+ }
1181
+ }
1182
+ await new Promise(r => setTimeout(r, intervalSec * 1000));
1183
+ }
1184
+ }
1185
+ finally {
1186
+ process.removeListener("SIGINT", cleanup);
1187
+ process.removeListener("SIGTERM", cleanup);
1188
+ }
1189
+ if (isJson())
1190
+ return printJson(jsonOk({ triggered: false, reason: "cancelled" }));
1191
+ console.log(chalk.yellow(`\n Trailing stop cancelled.\n`));
1192
+ });
1193
+ // ── PnL Tracker ──
1194
+ trade
1195
+ .command("pnl-track")
1196
+ .description("Live-monitor positions with real-time PnL updates")
1197
+ .option("--interval <sec>", "Refresh interval in seconds", "3")
1198
+ .option("--symbol <sym>", "Filter to a specific symbol")
1199
+ .action(async (opts) => {
1200
+ const intervalSec = parseInt(opts.interval);
1201
+ const filterSym = opts.symbol?.toUpperCase();
1202
+ const adapter = await getAdapter();
1203
+ console.log(chalk.cyan(`\n PnL Tracker | ${adapter.name} | Interval: ${intervalSec}s`));
1204
+ if (filterSym)
1205
+ console.log(chalk.cyan(` Filtering: ${filterSym}`));
1206
+ console.log(chalk.gray(` Press Ctrl+C to stop.\n`));
1207
+ let running = true;
1208
+ const cleanup = () => { running = false; };
1209
+ process.on("SIGINT", cleanup);
1210
+ process.on("SIGTERM", cleanup);
1211
+ try {
1212
+ while (running) {
1213
+ const [positions, balance] = await Promise.all([
1214
+ adapter.getPositions(),
1215
+ adapter.getBalance(),
1216
+ ]);
1217
+ let filtered = positions;
1218
+ if (filterSym) {
1219
+ filtered = positions.filter(p => symbolMatch(p.symbol, filterSym));
1220
+ }
1221
+ // Fetch funding payments (recent) for display
1222
+ let fundingBySymbol = {};
1223
+ try {
1224
+ const payments = await adapter.getFundingPayments(50);
1225
+ for (const fp of payments) {
1226
+ const sym = fp.symbol.toUpperCase();
1227
+ fundingBySymbol[sym] = (fundingBySymbol[sym] || 0) + parseFloat(fp.payment);
1228
+ }
1229
+ }
1230
+ catch {
1231
+ // funding payments may not be supported on all exchanges
1232
+ }
1233
+ console.clear();
1234
+ console.log(chalk.cyan.bold(`\n PnL Tracker — ${adapter.name} | ${new Date().toLocaleTimeString()}\n`));
1235
+ console.log(` Equity: $${formatUsd(balance.equity)} | Available: $${formatUsd(balance.available)} | Margin: $${formatUsd(balance.marginUsed)} | uPnL: $${formatUsd(balance.unrealizedPnl)}\n`);
1236
+ if (filtered.length === 0) {
1237
+ console.log(chalk.gray(` No open positions${filterSym ? ` for ${filterSym}` : ""}.`));
1238
+ }
1239
+ else {
1240
+ const { makeTable } = await import("../utils.js");
1241
+ const rows = filtered.map(p => {
1242
+ const entry = parseFloat(p.entryPrice);
1243
+ const mark = parseFloat(p.markPrice);
1244
+ const pnl = parseFloat(p.unrealizedPnl);
1245
+ const notional = parseFloat(p.size) * entry;
1246
+ const pnlPct = notional > 0 ? (pnl / notional) * 100 : 0;
1247
+ const funding = fundingBySymbol[p.symbol.toUpperCase()] || 0;
1248
+ const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
1249
+ const pctColor = pnlPct >= 0 ? chalk.green : chalk.red;
1250
+ return [
1251
+ chalk.white.bold(p.symbol),
1252
+ p.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
1253
+ p.size,
1254
+ `$${formatUsd(p.entryPrice)}`,
1255
+ `$${formatUsd(p.markPrice)}`,
1256
+ pnlColor(`${pnl >= 0 ? "+" : ""}$${pnl.toFixed(2)}`),
1257
+ pctColor(`${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(2)}%`),
1258
+ funding !== 0 ? `$${funding.toFixed(4)}` : "-",
1259
+ ];
1260
+ });
1261
+ console.log(makeTable(["Symbol", "Side", "Size", "Entry", "Mark", "PnL", "PnL%", "Funding"], rows));
1262
+ }
1263
+ console.log(chalk.gray(`\n Refreshing every ${intervalSec}s... Press Ctrl+C to stop.`));
1264
+ await new Promise(r => setTimeout(r, intervalSec * 1000));
1265
+ }
1266
+ }
1267
+ finally {
1268
+ process.removeListener("SIGINT", cleanup);
1269
+ process.removeListener("SIGTERM", cleanup);
1270
+ }
1271
+ console.log(chalk.yellow(`\n PnL tracker stopped.\n`));
1272
+ });
1273
+ }