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,921 @@
1
+ import chalk from "chalk";
2
+ import { makeTable, formatUsd, formatPnl, printJson, jsonOk } from "../../utils.js";
3
+ import { readExecutionLog, logExecution } from "../../execution-log.js";
4
+ import { computeAnnualSpread } from "../../funding.js";
5
+ import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
6
+ import { computeBasisRisk } from "../../arb-utils.js";
7
+ import { fetchAllBalances, computeRebalancePlan } from "../../rebalance.js";
8
+ import { EXCHANGE_TO_CHAIN, getBestQuote } from "../../bridge-engine.js";
9
+ import { computeEnhancedStats } from "../../arb-history-stats.js";
10
+ // ── Helpers ──
11
+ const EXCHANGES = ["hyperliquid", "lighter", "pacifica"];
12
+ const TAKER_FEE = 0.00035; // ~0.035% typical taker fee
13
+ function formatDuration(ms) {
14
+ const hours = Math.floor(ms / (1000 * 60 * 60));
15
+ const days = Math.floor(hours / 24);
16
+ const remainingHours = hours % 24;
17
+ if (days > 0)
18
+ return `${days}d ${remainingHours}h`;
19
+ if (hours > 0)
20
+ return `${hours}h`;
21
+ const minutes = Math.floor(ms / (1000 * 60));
22
+ return `${minutes}m`;
23
+ }
24
+ async function fetchFundingRatesMap() {
25
+ const rateMap = new Map();
26
+ const [pacAssets, hlAssets, ltDetails, ltFunding] = await Promise.all([
27
+ fetchPacificaPrices(),
28
+ fetchHyperliquidMeta(),
29
+ fetchLighterOrderBookDetails(),
30
+ fetchLighterFundingRates(),
31
+ ]);
32
+ const addRate = (sym, exchange, rate, markPrice) => {
33
+ if (!sym)
34
+ return;
35
+ if (!rateMap.has(sym))
36
+ rateMap.set(sym, []);
37
+ rateMap.get(sym).push({ exchange, rate, markPrice });
38
+ };
39
+ for (const p of pacAssets)
40
+ addRate(p.symbol, "pacifica", p.funding, p.mark);
41
+ for (const h of hlAssets)
42
+ addRate(h.symbol, "hyperliquid", h.funding, h.markPx);
43
+ // Lighter: join details + funding by marketId
44
+ const ltPriceMap = new Map(ltDetails.map(d => [d.marketId, d.lastTradePrice]));
45
+ const ltSymMap = new Map(ltDetails.map(d => [d.marketId, d.symbol]));
46
+ for (const fr of ltFunding) {
47
+ const sym = fr.symbol || ltSymMap.get(fr.marketId) || "";
48
+ const mp = fr.markPrice || ltPriceMap.get(fr.marketId) || 0;
49
+ addRate(sym, "lighter", fr.rate, mp);
50
+ }
51
+ return rateMap;
52
+ }
53
+ function findArbEntryForPair(symbol, longExchange, shortExchange) {
54
+ const entries = readExecutionLog({ type: "arb_entry", symbol });
55
+ const successful = entries.filter(e => e.status === "success");
56
+ if (!successful.length)
57
+ return null;
58
+ // Try matching by arbPairId first (new format)
59
+ if (longExchange && shortExchange) {
60
+ const pairId = `${symbol.toUpperCase()}:${longExchange}:${shortExchange}`;
61
+ const byPairId = successful.find(e => e.meta?.arbPairId === pairId);
62
+ if (byPairId)
63
+ return byPairId;
64
+ // Also try matching by exchange field (e.g. "pacifica+hyperliquid")
65
+ const byExchange = successful.find(e => e.meta?.longExchange === longExchange && e.meta?.shortExchange === shortExchange);
66
+ if (byExchange)
67
+ return byExchange;
68
+ }
69
+ // Fallback: return most recent entry for this symbol (legacy records without arbPairId)
70
+ return successful[0];
71
+ }
72
+ function getCurrentSpreadForSymbol(symbol, longExchange, shortExchange, rateMap) {
73
+ const rates = rateMap.get(symbol.toUpperCase());
74
+ if (!rates)
75
+ return null;
76
+ const longRate = rates.find(r => r.exchange === longExchange);
77
+ const shortRate = rates.find(r => r.exchange === shortExchange);
78
+ if (!longRate || !shortRate)
79
+ return null;
80
+ return computeAnnualSpread(shortRate.rate, shortRate.exchange, longRate.rate, longRate.exchange);
81
+ }
82
+ // ── Registration ──
83
+ export function registerArbManageCommands(program, getAdapterForExchange, isJson) {
84
+ const arb = program.commands.find(c => c.name() === "arb");
85
+ if (!arb)
86
+ return;
87
+ // ── arb status ──
88
+ arb
89
+ .command("status")
90
+ .description("Show open arb positions with PnL breakdown")
91
+ .action(async () => {
92
+ if (!isJson())
93
+ console.log(chalk.cyan("\n Checking arb positions across exchanges...\n"));
94
+ // Fetch positions from all exchanges
95
+ const allPositions = [];
96
+ for (const exName of EXCHANGES) {
97
+ try {
98
+ const adapter = await getAdapterForExchange(exName);
99
+ const positions = await adapter.getPositions();
100
+ for (const p of positions) {
101
+ allPositions.push({
102
+ exchange: exName,
103
+ symbol: p.symbol.replace("-PERP", "").toUpperCase(),
104
+ side: p.side,
105
+ size: Math.abs(Number(p.size)),
106
+ entryPrice: Number(p.entryPrice),
107
+ markPrice: Number(p.markPrice),
108
+ unrealizedPnl: Number(p.unrealizedPnl),
109
+ leverage: p.leverage,
110
+ });
111
+ }
112
+ }
113
+ catch {
114
+ // exchange not configured, skip
115
+ }
116
+ }
117
+ // Group by symbol to detect arb pairs
118
+ const bySymbol = new Map();
119
+ for (const p of allPositions) {
120
+ if (!bySymbol.has(p.symbol))
121
+ bySymbol.set(p.symbol, []);
122
+ bySymbol.get(p.symbol).push(p);
123
+ }
124
+ // Find arb pairs: same symbol, different exchanges, one long + one short
125
+ const arbPairs = [];
126
+ // Fetch current funding rates for spread calculation
127
+ let rateMap;
128
+ try {
129
+ rateMap = await fetchFundingRatesMap();
130
+ }
131
+ catch {
132
+ rateMap = new Map();
133
+ }
134
+ for (const [symbol, positions] of bySymbol) {
135
+ const longs = positions.filter(p => p.side === "long");
136
+ const shorts = positions.filter(p => p.side === "short");
137
+ // Match longs and shorts on different exchanges
138
+ for (const longPos of longs) {
139
+ for (const shortPos of shorts) {
140
+ if (longPos.exchange === shortPos.exchange)
141
+ continue;
142
+ // Look up entry info from execution log (match by pair)
143
+ const entryLog = findArbEntryForPair(symbol, longPos.exchange, shortPos.exchange);
144
+ const entrySpread = entryLog?.meta?.spread ?? null;
145
+ const entryTime = entryLog?.timestamp ?? null;
146
+ const holdDurationMs = entryTime ? Date.now() - new Date(entryTime).getTime() : null;
147
+ const holdDuration = holdDurationMs ? formatDuration(holdDurationMs) : null;
148
+ // Current spread
149
+ const currentSpread = getCurrentSpreadForSymbol(symbol, longPos.exchange, shortPos.exchange, rateMap);
150
+ // Position notional values
151
+ const longNotional = longPos.size * longPos.markPrice;
152
+ const shortNotional = shortPos.size * shortPos.markPrice;
153
+ const avgNotional = (longNotional + shortNotional) / 2;
154
+ // Estimated funding income (from hold time and current spread)
155
+ let estimatedFundingIncome = 0;
156
+ if (holdDurationMs && currentSpread) {
157
+ const holdHours = holdDurationMs / (1000 * 60 * 60);
158
+ // Annual spread % → hourly income on notional
159
+ estimatedFundingIncome = (currentSpread / 100) / (24 * 365) * avgNotional * holdHours;
160
+ }
161
+ // Estimated fees (entry + exit)
162
+ const entryFees = (longPos.size * longPos.entryPrice + shortPos.size * shortPos.entryPrice) * TAKER_FEE;
163
+ const exitFees = (longNotional + shortNotional) * TAKER_FEE;
164
+ const totalFees = entryFees + exitFees;
165
+ // Net PnL
166
+ const totalUpnl = longPos.unrealizedPnl + shortPos.unrealizedPnl;
167
+ const netPnl = totalUpnl + estimatedFundingIncome - totalFees;
168
+ arbPairs.push({
169
+ symbol,
170
+ longExchange: longPos.exchange,
171
+ shortExchange: shortPos.exchange,
172
+ longPosition: {
173
+ side: "long",
174
+ size: longPos.size,
175
+ entryPrice: longPos.entryPrice,
176
+ markPrice: longPos.markPrice,
177
+ unrealizedPnl: longPos.unrealizedPnl,
178
+ leverage: longPos.leverage,
179
+ notionalUsd: longNotional,
180
+ },
181
+ shortPosition: {
182
+ side: "short",
183
+ size: shortPos.size,
184
+ entryPrice: shortPos.entryPrice,
185
+ markPrice: shortPos.markPrice,
186
+ unrealizedPnl: shortPos.unrealizedPnl,
187
+ leverage: shortPos.leverage,
188
+ notionalUsd: shortNotional,
189
+ },
190
+ entrySpread: entrySpread !== null ? Number(entrySpread) : null,
191
+ currentSpread,
192
+ holdDuration,
193
+ holdDurationMs,
194
+ estimatedFundingIncome,
195
+ estimatedFees: totalFees,
196
+ unrealizedPnl: totalUpnl,
197
+ netPnl,
198
+ });
199
+ }
200
+ }
201
+ }
202
+ if (isJson()) {
203
+ return printJson(jsonOk(arbPairs));
204
+ }
205
+ if (arbPairs.length === 0) {
206
+ console.log(chalk.gray(" No open arb positions found.\n"));
207
+ return;
208
+ }
209
+ console.log(chalk.cyan.bold(" Open Arb Positions\n"));
210
+ const rows = arbPairs.map(p => {
211
+ const spreadStr = p.currentSpread !== null ? `${p.currentSpread.toFixed(1)}%` : "-";
212
+ const entrySpreadStr = p.entrySpread !== null ? `${p.entrySpread.toFixed(1)}%` : "-";
213
+ const holdStr = p.holdDuration ?? "-";
214
+ const avgNotional = (p.longPosition.notionalUsd + p.shortPosition.notionalUsd) / 2;
215
+ // Compute basis risk from mark prices
216
+ const basis = computeBasisRisk(p.longPosition.markPrice, p.shortPosition.markPrice);
217
+ const basisStr = basis.divergencePct > 0
218
+ ? (basis.warning
219
+ ? chalk.red(`${basis.divergencePct.toFixed(1)}%`)
220
+ : chalk.gray(`${basis.divergencePct.toFixed(1)}%`))
221
+ : chalk.gray("-");
222
+ return [
223
+ chalk.white.bold(p.symbol),
224
+ chalk.green(p.longExchange),
225
+ chalk.red(p.shortExchange),
226
+ `$${formatUsd(avgNotional)}`,
227
+ `$${p.longPosition.entryPrice.toFixed(2)} / $${p.shortPosition.entryPrice.toFixed(2)}`,
228
+ `$${p.longPosition.markPrice.toFixed(2)}`,
229
+ formatPnl(p.unrealizedPnl),
230
+ chalk.yellow(`$${p.estimatedFundingIncome.toFixed(4)}`),
231
+ `${entrySpreadStr} -> ${spreadStr}`,
232
+ basisStr,
233
+ holdStr,
234
+ formatPnl(p.netPnl),
235
+ ];
236
+ });
237
+ console.log(makeTable(["Symbol", "Long", "Short", "Size", "Entry", "Mark", "uPnL", "Funding", "Spread", "Basis", "Hold", "Net PnL"], rows));
238
+ // Summary
239
+ const totalUpnl = arbPairs.reduce((s, p) => s + p.unrealizedPnl, 0);
240
+ const totalFunding = arbPairs.reduce((s, p) => s + p.estimatedFundingIncome, 0);
241
+ const totalFees = arbPairs.reduce((s, p) => s + p.estimatedFees, 0);
242
+ const totalNet = arbPairs.reduce((s, p) => s + p.netPnl, 0);
243
+ console.log(chalk.white.bold("\n Summary"));
244
+ console.log(` Positions: ${arbPairs.length}`);
245
+ console.log(` Unrealized PnL: ${formatPnl(totalUpnl)}`);
246
+ console.log(` Est. Funding: ${chalk.yellow(`$${totalFunding.toFixed(4)}`)}`);
247
+ console.log(` Est. Fees: ${chalk.red(`-$${totalFees.toFixed(4)}`)}`);
248
+ console.log(` Net PnL: ${formatPnl(totalNet)}`);
249
+ console.log(chalk.gray(` (Fees assume ${(TAKER_FEE * 100).toFixed(3)}% taker for entry + exit.)\n`));
250
+ });
251
+ // ── arb close ──
252
+ arb
253
+ .command("close <symbol>")
254
+ .description("Manually close an arb position on both exchanges")
255
+ .option("--dry-run", "Show what would happen without executing")
256
+ .option("--pair <pair>", "Specify arb pair as longExchange:shortExchange (e.g. pacifica:hyperliquid)")
257
+ .action(async (symbol, opts) => {
258
+ const sym = symbol.toUpperCase();
259
+ const dryRun = !!opts.dryRun || process.argv.includes("--dry-run");
260
+ if (!isJson()) {
261
+ console.log(chalk.cyan(`\n Closing arb position for ${sym}...\n`));
262
+ if (dryRun)
263
+ console.log(chalk.yellow(" Mode: DRY RUN (no trades will be executed)\n"));
264
+ }
265
+ // Find positions for this symbol across all exchanges
266
+ const allPositions = [];
267
+ for (const exName of EXCHANGES) {
268
+ try {
269
+ const adapter = await getAdapterForExchange(exName);
270
+ const positions = await adapter.getPositions();
271
+ for (const p of positions) {
272
+ const normalized = p.symbol.replace("-PERP", "").toUpperCase();
273
+ if (normalized === sym) {
274
+ allPositions.push({
275
+ exchange: exName,
276
+ symbol: normalized,
277
+ rawSymbol: p.symbol,
278
+ side: p.side,
279
+ size: Math.abs(Number(p.size)),
280
+ entryPrice: Number(p.entryPrice),
281
+ markPrice: Number(p.markPrice),
282
+ unrealizedPnl: Number(p.unrealizedPnl),
283
+ });
284
+ }
285
+ }
286
+ }
287
+ catch {
288
+ // exchange not configured, skip
289
+ }
290
+ }
291
+ // Find all possible arb pairs (long on one exchange, short on another)
292
+ const possiblePairs = [];
293
+ const longs = allPositions.filter(p => p.side === "long");
294
+ const shorts = allPositions.filter(p => p.side === "short");
295
+ for (const l of longs) {
296
+ for (const s of shorts) {
297
+ if (l.exchange !== s.exchange)
298
+ possiblePairs.push({ long: l, short: s });
299
+ }
300
+ }
301
+ // If --pair specified, filter to that specific pair
302
+ let longPos;
303
+ let shortPos;
304
+ if (opts.pair) {
305
+ const [longEx, shortEx] = opts.pair.split(":");
306
+ const match = possiblePairs.find(p => p.long.exchange === longEx && p.short.exchange === shortEx);
307
+ if (match) {
308
+ longPos = match.long;
309
+ shortPos = match.short;
310
+ }
311
+ }
312
+ else if (possiblePairs.length === 1) {
313
+ longPos = possiblePairs[0].long;
314
+ shortPos = possiblePairs[0].short;
315
+ }
316
+ else if (possiblePairs.length > 1) {
317
+ // Multiple pairs found — require explicit --pair selection
318
+ const msg = `Multiple arb pairs found for ${sym}. Use --pair to specify which one to close.`;
319
+ if (isJson()) {
320
+ return printJson(jsonOk({
321
+ error: msg,
322
+ pairs: possiblePairs.map(p => ({
323
+ arbPairId: `${sym}:${p.long.exchange}:${p.short.exchange}`,
324
+ longExchange: p.long.exchange,
325
+ shortExchange: p.short.exchange,
326
+ longSize: p.long.size,
327
+ shortSize: p.short.size,
328
+ })),
329
+ }));
330
+ }
331
+ console.log(chalk.red(` ${msg}\n`));
332
+ console.log(chalk.white(" Available pairs:"));
333
+ for (const p of possiblePairs) {
334
+ console.log(chalk.gray(` --pair ${p.long.exchange}:${p.short.exchange} (long ${p.long.size} / short ${p.short.size})`));
335
+ }
336
+ console.log();
337
+ return;
338
+ }
339
+ if (!longPos || !shortPos) {
340
+ const msg = `No arb pair found for ${sym}. Need long and short on different exchanges.`;
341
+ if (isJson())
342
+ return printJson(jsonOk({ error: msg, positions: allPositions }));
343
+ console.log(chalk.red(` ${msg}`));
344
+ if (allPositions.length > 0) {
345
+ console.log(chalk.gray(` Found ${allPositions.length} position(s) but no matching arb pair:`));
346
+ for (const p of allPositions) {
347
+ console.log(chalk.gray(` ${p.side.toUpperCase()} ${p.exchange} size=${p.size}`));
348
+ }
349
+ }
350
+ console.log();
351
+ return;
352
+ }
353
+ // Calculate estimated PnL
354
+ const totalUpnl = longPos.unrealizedPnl + shortPos.unrealizedPnl;
355
+ const entryFees = (longPos.size * longPos.entryPrice + shortPos.size * shortPos.entryPrice) * TAKER_FEE;
356
+ const exitFees = (longPos.size * longPos.markPrice + shortPos.size * shortPos.markPrice) * TAKER_FEE;
357
+ const totalFees = entryFees + exitFees;
358
+ const netPnl = totalUpnl - totalFees;
359
+ if (!isJson()) {
360
+ console.log(chalk.white.bold(` ${sym} Arb Position`));
361
+ console.log(` Long: ${longPos.exchange} | size: ${longPos.size} | entry: $${longPos.entryPrice.toFixed(4)} | mark: $${longPos.markPrice.toFixed(4)} | uPnL: ${formatPnl(longPos.unrealizedPnl)}`);
362
+ console.log(` Short: ${shortPos.exchange} | size: ${shortPos.size} | entry: $${shortPos.entryPrice.toFixed(4)} | mark: $${shortPos.markPrice.toFixed(4)} | uPnL: ${formatPnl(shortPos.unrealizedPnl)}`);
363
+ console.log();
364
+ console.log(` Total uPnL: ${formatPnl(totalUpnl)}`);
365
+ console.log(` Est. Fees: ${chalk.red(`-$${totalFees.toFixed(4)}`)}`);
366
+ console.log(` Net PnL: ${formatPnl(netPnl)}`);
367
+ console.log();
368
+ }
369
+ if (dryRun) {
370
+ const result = {
371
+ dryRun: true,
372
+ symbol: sym,
373
+ longExchange: longPos.exchange,
374
+ shortExchange: shortPos.exchange,
375
+ longSize: longPos.size,
376
+ shortSize: shortPos.size,
377
+ unrealizedPnl: totalUpnl,
378
+ estimatedFees: totalFees,
379
+ netPnl,
380
+ actions: [
381
+ { exchange: longPos.exchange, action: "sell", symbol: longPos.rawSymbol, size: String(longPos.size) },
382
+ { exchange: shortPos.exchange, action: "buy", symbol: shortPos.rawSymbol, size: String(shortPos.size) },
383
+ ],
384
+ };
385
+ if (isJson())
386
+ return printJson(jsonOk(result));
387
+ console.log(chalk.yellow(" Would execute:"));
388
+ console.log(chalk.yellow(` SELL ${longPos.size} ${longPos.rawSymbol} on ${longPos.exchange} (close long)`));
389
+ console.log(chalk.yellow(` BUY ${shortPos.size} ${shortPos.rawSymbol} on ${shortPos.exchange} (close short)\n`));
390
+ return;
391
+ }
392
+ // Execute close on both exchanges
393
+ const results = [];
394
+ // Close both legs concurrently
395
+ const closePromises = [
396
+ (async () => {
397
+ try {
398
+ const adapter = await getAdapterForExchange(longPos.exchange);
399
+ await adapter.marketOrder(longPos.rawSymbol, "sell", String(longPos.size));
400
+ results.push({ exchange: longPos.exchange, action: "sell (close long)", status: "success" });
401
+ if (!isJson())
402
+ console.log(chalk.green(` Closed long on ${longPos.exchange}: SELL ${longPos.size} ${sym}`));
403
+ }
404
+ catch (err) {
405
+ const msg = err instanceof Error ? err.message : String(err);
406
+ results.push({ exchange: longPos.exchange, action: "sell (close long)", status: "failed", error: msg });
407
+ if (!isJson())
408
+ console.error(chalk.red(` Failed to close long on ${longPos.exchange}: ${msg}`));
409
+ }
410
+ })(),
411
+ (async () => {
412
+ try {
413
+ const adapter = await getAdapterForExchange(shortPos.exchange);
414
+ await adapter.marketOrder(shortPos.rawSymbol, "buy", String(shortPos.size));
415
+ results.push({ exchange: shortPos.exchange, action: "buy (close short)", status: "success" });
416
+ if (!isJson())
417
+ console.log(chalk.green(` Closed short on ${shortPos.exchange}: BUY ${shortPos.size} ${sym}`));
418
+ }
419
+ catch (err) {
420
+ const msg = err instanceof Error ? err.message : String(err);
421
+ results.push({ exchange: shortPos.exchange, action: "buy (close short)", status: "failed", error: msg });
422
+ if (!isJson())
423
+ console.error(chalk.red(` Failed to close short on ${shortPos.exchange}: ${msg}`));
424
+ }
425
+ })(),
426
+ ];
427
+ await Promise.all(closePromises);
428
+ const allSuccess = results.every(r => r.status === "success");
429
+ const anySuccess = results.some(r => r.status === "success");
430
+ // Log to execution log
431
+ const arbPairId = `${sym}:${longPos.exchange}:${shortPos.exchange}`;
432
+ logExecution({
433
+ type: "arb_close",
434
+ exchange: `${longPos.exchange}+${shortPos.exchange}`,
435
+ symbol: sym,
436
+ side: "close",
437
+ size: String(Math.max(longPos.size, shortPos.size)),
438
+ status: allSuccess ? "success" : "failed",
439
+ dryRun: false,
440
+ error: allSuccess ? undefined : results.filter(r => r.error).map(r => `${r.exchange}: ${r.error}`).join("; "),
441
+ meta: {
442
+ arbPairId,
443
+ longExchange: longPos.exchange,
444
+ shortExchange: shortPos.exchange,
445
+ unrealizedPnl: totalUpnl,
446
+ estimatedFees: totalFees,
447
+ netPnl,
448
+ results,
449
+ exitReason: "manual",
450
+ },
451
+ });
452
+ if (isJson()) {
453
+ return printJson(jsonOk({
454
+ symbol: sym,
455
+ longExchange: longPos.exchange,
456
+ shortExchange: shortPos.exchange,
457
+ status: allSuccess ? "success" : anySuccess ? "partial" : "failed",
458
+ unrealizedPnl: totalUpnl,
459
+ estimatedFees: totalFees,
460
+ netPnl,
461
+ results,
462
+ }));
463
+ }
464
+ if (!allSuccess && anySuccess) {
465
+ console.log(chalk.yellow(`\n Warning: Partial close — one leg failed. Manual intervention may be needed.\n`));
466
+ }
467
+ else if (allSuccess) {
468
+ console.log(chalk.green(`\n Arb position ${sym} closed successfully.\n`));
469
+ }
470
+ else {
471
+ console.log(chalk.red(`\n Failed to close arb position ${sym}. Both legs failed.\n`));
472
+ }
473
+ });
474
+ // ── arb history ──
475
+ arb
476
+ .command("history")
477
+ .description("Past arb trade performance and statistics")
478
+ .option("--period <days>", "Number of days to look back", "30")
479
+ .action(async (opts) => {
480
+ const periodDays = parseInt(opts.period);
481
+ const sinceDate = new Date(Date.now() - periodDays * 24 * 60 * 60 * 1000).toISOString();
482
+ if (!isJson())
483
+ console.log(chalk.cyan(`\n Arb trade history (last ${periodDays} days)\n`));
484
+ // Read arb-related execution log entries
485
+ const arbEntries = readExecutionLog({ since: sinceDate })
486
+ .filter(r => r.type === "arb_entry" || r.type === "arb_close");
487
+ if (arbEntries.length === 0) {
488
+ const result = {
489
+ trades: [],
490
+ summary: {
491
+ totalTrades: 0,
492
+ completedTrades: 0,
493
+ winRate: 0,
494
+ avgHoldTime: null,
495
+ totalNetPnl: 0,
496
+ bestTrade: null,
497
+ worstTrade: null,
498
+ avgEntrySpread: 0,
499
+ avgExitSpread: 0,
500
+ avgSpreadDecay: 0,
501
+ byExchangePair: [],
502
+ byTimeOfDay: [],
503
+ optimalHoldTime: null,
504
+ },
505
+ period: periodDays,
506
+ };
507
+ if (isJson())
508
+ return printJson(jsonOk(result));
509
+ console.log(chalk.gray(" No arb trades found in this period.\n"));
510
+ return;
511
+ }
512
+ // Group entries by arbPairId (or symbol fallback for legacy) to match entry/exit pairs
513
+ const entryMap = new Map();
514
+ const closeMap = new Map();
515
+ function getPairKey(record) {
516
+ // Use arbPairId if available (new format), fallback to symbol (legacy)
517
+ const arbPairId = record.meta?.arbPairId;
518
+ if (arbPairId)
519
+ return arbPairId;
520
+ return record.symbol.toUpperCase();
521
+ }
522
+ for (const entry of arbEntries) {
523
+ const key = getPairKey(entry);
524
+ if (entry.type === "arb_entry") {
525
+ if (!entryMap.has(key))
526
+ entryMap.set(key, []);
527
+ entryMap.get(key).push(entry);
528
+ }
529
+ else {
530
+ if (!closeMap.has(key))
531
+ closeMap.set(key, []);
532
+ closeMap.get(key).push(entry);
533
+ }
534
+ }
535
+ // Build trade history by pairing entries with closes
536
+ const trades = [];
537
+ for (const [symbol, entries] of entryMap) {
538
+ const closes = closeMap.get(symbol) ?? [];
539
+ // Sort entries oldest first for matching
540
+ entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
541
+ closes.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
542
+ for (let i = 0; i < entries.length; i++) {
543
+ const entry = entries[i];
544
+ // Find matching close (first close after this entry)
545
+ const entryTime = new Date(entry.timestamp).getTime();
546
+ const matchingClose = closes.find(c => new Date(c.timestamp).getTime() > entryTime);
547
+ const exchanges = entry.exchange;
548
+ const entrySpread = entry.meta?.spread ?? null;
549
+ if (matchingClose) {
550
+ // Remove matched close to avoid double-matching
551
+ const closeIdx = closes.indexOf(matchingClose);
552
+ closes.splice(closeIdx, 1);
553
+ const closeTime = new Date(matchingClose.timestamp).getTime();
554
+ const holdMs = closeTime - entryTime;
555
+ const holdHours = holdMs / (1000 * 60 * 60);
556
+ // Estimate PnL from metadata
557
+ const exitSpread = matchingClose.meta?.currentSpread ?? null;
558
+ const netPnl = matchingClose.meta?.netPnl ?? null;
559
+ const upnl = matchingClose.meta?.unrealizedPnl ?? null;
560
+ const exitReason = matchingClose.meta?.exitReason ?? null;
561
+ // Estimate funding income based on hold time and entry spread
562
+ const avgSpread = entrySpread ? entrySpread : 0;
563
+ const sizeUsd = entry.meta?.markPrice
564
+ ? Number(entry.size) * Number(entry.meta.markPrice)
565
+ : 0;
566
+ const estimatedFunding = sizeUsd > 0
567
+ ? (avgSpread / 100) / (24 * 365) * sizeUsd * holdHours
568
+ : 0;
569
+ // Fees estimate
570
+ const fees = sizeUsd * TAKER_FEE * 2 * 2; // entry + exit, both legs
571
+ trades.push({
572
+ symbol,
573
+ exchanges,
574
+ entryDate: entry.timestamp,
575
+ exitDate: matchingClose.timestamp,
576
+ holdDuration: formatDuration(holdMs),
577
+ holdDurationMs: holdMs,
578
+ entrySpread,
579
+ exitSpread,
580
+ size: entry.size,
581
+ grossReturn: upnl !== null ? Number(upnl) : 0,
582
+ fees,
583
+ fundingIncome: estimatedFunding,
584
+ netReturn: netPnl !== null ? Number(netPnl) : (upnl !== null ? Number(upnl) + estimatedFunding - fees : 0),
585
+ status: matchingClose.status === "success" ? "completed" : "failed",
586
+ exitReason,
587
+ });
588
+ }
589
+ else {
590
+ // Open trade (no matching close)
591
+ const holdMs = Date.now() - entryTime;
592
+ trades.push({
593
+ symbol,
594
+ exchanges,
595
+ entryDate: entry.timestamp,
596
+ exitDate: null,
597
+ holdDuration: formatDuration(holdMs),
598
+ holdDurationMs: holdMs,
599
+ entrySpread,
600
+ exitSpread: null,
601
+ size: entry.size,
602
+ grossReturn: 0,
603
+ fees: 0,
604
+ fundingIncome: 0,
605
+ netReturn: 0,
606
+ status: "open",
607
+ exitReason: null,
608
+ });
609
+ }
610
+ }
611
+ }
612
+ // Sort by entry date, newest first
613
+ trades.sort((a, b) => new Date(b.entryDate).getTime() - new Date(a.entryDate).getTime());
614
+ // Compute summary statistics
615
+ const completedTrades = trades.filter(t => t.status === "completed");
616
+ const winners = completedTrades.filter(t => t.netReturn > 0);
617
+ const totalNetPnl = completedTrades.reduce((s, t) => s + t.netReturn, 0);
618
+ const totalFunding = completedTrades.reduce((s, t) => s + t.fundingIncome, 0);
619
+ const totalFees = completedTrades.reduce((s, t) => s + t.fees, 0);
620
+ const avgHoldMs = completedTrades.length > 0
621
+ ? completedTrades.reduce((s, t) => s + t.holdDurationMs, 0) / completedTrades.length
622
+ : 0;
623
+ const bestTrade = completedTrades.length > 0
624
+ ? completedTrades.reduce((best, t) => t.netReturn > best.netReturn ? t : best)
625
+ : null;
626
+ const worstTrade = completedTrades.length > 0
627
+ ? completedTrades.reduce((worst, t) => t.netReturn < worst.netReturn ? t : worst)
628
+ : null;
629
+ // Compute enhanced analytics
630
+ const statsInput = trades.map(t => ({
631
+ symbol: t.symbol,
632
+ exchanges: t.exchanges,
633
+ entryDate: t.entryDate,
634
+ exitDate: t.exitDate,
635
+ holdDurationMs: t.holdDurationMs,
636
+ entrySpread: t.entrySpread,
637
+ exitSpread: t.exitSpread,
638
+ netReturn: t.netReturn,
639
+ status: t.status,
640
+ }));
641
+ const enhanced = computeEnhancedStats(statsInput);
642
+ const summary = {
643
+ totalTrades: trades.length,
644
+ completedTrades: completedTrades.length,
645
+ openTrades: trades.filter(t => t.status === "open").length,
646
+ failedTrades: trades.filter(t => t.status === "failed").length,
647
+ winRate: completedTrades.length > 0 ? (winners.length / completedTrades.length) * 100 : 0,
648
+ avgHoldTime: avgHoldMs > 0 ? formatDuration(avgHoldMs) : null,
649
+ avgHoldTimeMs: avgHoldMs,
650
+ totalNetPnl,
651
+ totalFundingIncome: totalFunding,
652
+ totalFees,
653
+ bestTrade: bestTrade ? { symbol: bestTrade.symbol, netReturn: bestTrade.netReturn } : null,
654
+ worstTrade: worstTrade ? { symbol: worstTrade.symbol, netReturn: worstTrade.netReturn } : null,
655
+ avgEntrySpread: enhanced.avgEntrySpread,
656
+ avgExitSpread: enhanced.avgExitSpread,
657
+ avgSpreadDecay: enhanced.avgSpreadDecay,
658
+ byExchangePair: enhanced.byExchangePair,
659
+ byTimeOfDay: enhanced.byTimeOfDay,
660
+ optimalHoldTime: enhanced.optimalHoldTime,
661
+ };
662
+ if (isJson()) {
663
+ return printJson(jsonOk({ trades, summary, period: periodDays }));
664
+ }
665
+ // Display trade history table
666
+ if (trades.length > 0) {
667
+ console.log(chalk.cyan.bold(" Trade History\n"));
668
+ const rows = trades.map(t => {
669
+ const statusIcon = t.status === "completed" ? chalk.green("DONE")
670
+ : t.status === "open" ? chalk.yellow("OPEN")
671
+ : chalk.red("FAIL");
672
+ const entryDate = new Date(t.entryDate).toLocaleDateString();
673
+ const exitDate = t.exitDate ? new Date(t.exitDate).toLocaleDateString() : "-";
674
+ return [
675
+ chalk.white.bold(t.symbol),
676
+ t.exchanges,
677
+ entryDate,
678
+ exitDate,
679
+ t.holdDuration,
680
+ t.entrySpread !== null ? `${t.entrySpread.toFixed(1)}%` : "-",
681
+ t.size,
682
+ formatPnl(t.grossReturn),
683
+ chalk.yellow(`$${t.fundingIncome.toFixed(4)}`),
684
+ chalk.red(`-$${t.fees.toFixed(4)}`),
685
+ formatPnl(t.netReturn),
686
+ statusIcon,
687
+ t.exitReason ?? "-",
688
+ ];
689
+ });
690
+ console.log(makeTable(["Symbol", "Exchanges", "Entry", "Exit", "Hold", "Spread", "Size", "Gross", "Funding", "Fees", "Net", "Status", "Reason"], rows));
691
+ }
692
+ // Summary
693
+ console.log(chalk.cyan.bold("\n Summary Statistics\n"));
694
+ console.log(` Period: Last ${periodDays} days`);
695
+ console.log(` Total trades: ${summary.totalTrades} (${summary.completedTrades} completed, ${summary.openTrades} open, ${summary.failedTrades} failed)`);
696
+ console.log(` Win rate: ${summary.winRate.toFixed(1)}%`);
697
+ console.log(` Avg hold time: ${summary.avgHoldTime ?? "-"}`);
698
+ console.log(` Total net PnL: ${formatPnl(summary.totalNetPnl)}`);
699
+ console.log(` Total funding: ${chalk.yellow(`$${summary.totalFundingIncome.toFixed(4)}`)}`);
700
+ console.log(` Total fees: ${chalk.red(`-$${summary.totalFees.toFixed(4)}`)}`);
701
+ if (summary.bestTrade) {
702
+ console.log(` Best trade: ${summary.bestTrade.symbol} ${formatPnl(summary.bestTrade.netReturn)}`);
703
+ }
704
+ if (summary.worstTrade) {
705
+ console.log(` Worst trade: ${summary.worstTrade.symbol} ${formatPnl(summary.worstTrade.netReturn)}`);
706
+ }
707
+ // ── Exchange Pair Performance ──
708
+ if (enhanced.byExchangePair.length > 0) {
709
+ console.log(chalk.cyan.bold("\n Exchange Pair Performance\n"));
710
+ const pairRows = enhanced.byExchangePair.map(p => [
711
+ chalk.white.bold(p.pair),
712
+ String(p.trades),
713
+ `${p.winRate.toFixed(0)}%`,
714
+ formatPnl(p.avgNetPnl),
715
+ p.avgHoldTime,
716
+ ]);
717
+ console.log(makeTable(["Pair", "Trades", "Win%", "Avg PnL", "Avg Hold"], pairRows));
718
+ }
719
+ // ── Time of Day Performance ──
720
+ if (enhanced.byTimeOfDay.length > 0) {
721
+ console.log(chalk.cyan.bold("\n Time of Day Performance\n"));
722
+ const todRows = enhanced.byTimeOfDay.map(b => [
723
+ chalk.white(b.bucket),
724
+ String(b.trades),
725
+ `${b.winRate.toFixed(0)}%`,
726
+ formatPnl(b.avgNetPnl),
727
+ ]);
728
+ console.log(makeTable(["UTC", "Trades", "Win%", "Avg PnL"], todRows));
729
+ }
730
+ // ── Spread Decay & Optimal Hold ──
731
+ if (enhanced.optimalHoldTime) {
732
+ console.log(` Optimal hold time: ~${enhanced.optimalHoldTime} (median of winning trades)`);
733
+ }
734
+ if (enhanced.avgEntrySpread > 0 && enhanced.avgExitSpread >= 0) {
735
+ console.log(` Avg spread decay: ${enhanced.avgEntrySpread.toFixed(1)}% -> ${enhanced.avgExitSpread.toFixed(1)}% over avg ${summary.avgHoldTime ?? "-"}`);
736
+ }
737
+ console.log();
738
+ });
739
+ // ── arb rebalance ──
740
+ arb
741
+ .command("rebalance")
742
+ .description("Cross-exchange balance rebalancing for arb")
743
+ .option("--check", "Show current balance distribution")
744
+ .option("--target <ratio>", "Target distribution ratio (e.g., '50:50' for 2 exchanges, '33:33:33' for 3)")
745
+ .option("--amount <usd>", "Total amount to rebalance")
746
+ .option("--dry-run", "Show plan without executing")
747
+ .option("--exchanges <list>", "Comma-separated exchanges", "lighter,pacifica,hyperliquid")
748
+ .action(async (opts) => {
749
+ const exchangeNames = opts.exchanges.split(",").map(e => e.trim());
750
+ const adapters = new Map();
751
+ for (const name of exchangeNames) {
752
+ try {
753
+ adapters.set(name, await getAdapterForExchange(name));
754
+ }
755
+ catch { /* skip unavailable */ }
756
+ }
757
+ if (adapters.size === 0) {
758
+ if (isJson())
759
+ return printJson(jsonOk({ error: "No exchanges available" }));
760
+ console.error(chalk.red("\n No exchanges available. Check credentials.\n"));
761
+ return;
762
+ }
763
+ // Default to --check if no action specified
764
+ if (!opts.target && !opts.check) {
765
+ opts.check = true;
766
+ }
767
+ const snapshots = await fetchAllBalances(adapters);
768
+ const totalEquity = snapshots.reduce((s, e) => s + e.equity, 0);
769
+ const totalAvailable = snapshots.reduce((s, e) => s + e.available, 0);
770
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
771
+ const exChain = (e) => e === "pacifica" ? "Solana" : e === "hyperliquid" ? "Hyperliquid" : e === "lighter" ? "Arbitrum" : "unknown";
772
+ if (opts.check) {
773
+ // Show current balance distribution
774
+ if (isJson()) {
775
+ return printJson(jsonOk({
776
+ balances: snapshots.map(s => ({
777
+ exchange: s.exchange,
778
+ abbr: exAbbr(s.exchange),
779
+ chain: exChain(s.exchange),
780
+ equity: s.equity,
781
+ available: s.available,
782
+ marginUsed: s.marginUsed,
783
+ unrealizedPnl: s.unrealizedPnl,
784
+ allocationPct: totalEquity > 0 ? (s.equity / totalEquity) * 100 : 0,
785
+ })),
786
+ totalEquity,
787
+ totalAvailable,
788
+ }));
789
+ }
790
+ console.log(chalk.cyan("\n Cross-Exchange Balance Distribution\n"));
791
+ const rows = snapshots.map(s => {
792
+ const pct = totalEquity > 0 ? ((s.equity / totalEquity) * 100).toFixed(1) : "0.0";
793
+ return [
794
+ chalk.white.bold(exAbbr(s.exchange).padEnd(4)),
795
+ chalk.gray(exChain(s.exchange).padEnd(12)),
796
+ `$${formatUsd(s.equity)}`,
797
+ `$${formatUsd(s.available)}`,
798
+ `$${formatUsd(s.marginUsed)}`,
799
+ s.unrealizedPnl >= 0
800
+ ? chalk.green(`+$${formatUsd(s.unrealizedPnl)}`)
801
+ : chalk.red(`-$${formatUsd(Math.abs(s.unrealizedPnl))}`),
802
+ `${pct}%`,
803
+ ];
804
+ });
805
+ console.log(makeTable(["Exch", "Chain", "Equity", "Available", "Margin", "uPnL", "Alloc%"], rows));
806
+ console.log(chalk.cyan.bold(" Totals"));
807
+ console.log(` Total Equity: $${formatUsd(totalEquity)}`);
808
+ console.log(` Total Available: $${formatUsd(totalAvailable)}`);
809
+ console.log(` Exchanges: ${snapshots.length}\n`);
810
+ return;
811
+ }
812
+ // Parse target ratios
813
+ if (!opts.target) {
814
+ console.error(chalk.red(" --target required (e.g., '50:50' or '33:33:33')"));
815
+ return;
816
+ }
817
+ const ratios = opts.target.split(":").map(Number);
818
+ if (ratios.length !== snapshots.length || ratios.some(isNaN)) {
819
+ console.error(chalk.red(` Target ratio must have ${snapshots.length} parts (one per exchange), got '${opts.target}'`));
820
+ return;
821
+ }
822
+ const ratioSum = ratios.reduce((a, b) => a + b, 0);
823
+ const weights = {};
824
+ snapshots.forEach((s, i) => {
825
+ weights[s.exchange] = ratios[i] / ratioSum;
826
+ });
827
+ // Compute plan
828
+ const plan = computeRebalancePlan(snapshots, { weights, minMove: 10, reserve: 10 });
829
+ if (plan.moves.length === 0) {
830
+ if (isJson())
831
+ return printJson(jsonOk({ status: "balanced", moves: [], snapshots }));
832
+ console.log(chalk.green("\n Already balanced -- no moves needed.\n"));
833
+ return;
834
+ }
835
+ // If --amount specified, scale moves proportionally
836
+ let moves = plan.moves;
837
+ if (opts.amount) {
838
+ const requestedAmount = parseFloat(opts.amount);
839
+ const totalMoveAmount = moves.reduce((s, m) => s + m.amount, 0);
840
+ if (totalMoveAmount > 0) {
841
+ const scale = Math.min(1, requestedAmount / totalMoveAmount);
842
+ moves = moves.map(m => ({ ...m, amount: Math.floor(m.amount * scale) })).filter(m => m.amount >= 10);
843
+ }
844
+ }
845
+ // Get bridge route info for each move
846
+ const moveDetails = await Promise.all(moves.map(async (m) => {
847
+ const srcChain = EXCHANGE_TO_CHAIN[m.from] ?? "unknown";
848
+ const dstChain = EXCHANGE_TO_CHAIN[m.to] ?? "unknown";
849
+ let bridgeFee = 0;
850
+ let bridgeProvider = "same-chain";
851
+ let bridgeTime = "instant";
852
+ if (srcChain !== dstChain && srcChain !== "unknown" && dstChain !== "unknown") {
853
+ try {
854
+ const quote = await getBestQuote(srcChain, dstChain, m.amount, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000");
855
+ bridgeFee = quote.fee;
856
+ bridgeProvider = quote.provider;
857
+ bridgeTime = `~${Math.ceil(quote.estimatedTime / 60)}min`;
858
+ }
859
+ catch {
860
+ bridgeFee = 0.5; // fallback estimate
861
+ bridgeProvider = "cctp";
862
+ bridgeTime = "~3min";
863
+ }
864
+ }
865
+ return {
866
+ ...m,
867
+ srcChain,
868
+ dstChain,
869
+ bridgeFee,
870
+ bridgeProvider,
871
+ bridgeTime,
872
+ };
873
+ }));
874
+ if (isJson()) {
875
+ return printJson(jsonOk({
876
+ status: opts.dryRun ? "dry_run" : "planned",
877
+ target: weights,
878
+ moves: moveDetails,
879
+ snapshots,
880
+ totalEquity,
881
+ }));
882
+ }
883
+ console.log(chalk.cyan("\n Rebalance Plan\n"));
884
+ // Show current vs target
885
+ const stateRows = snapshots.map(s => {
886
+ const targetPct = (weights[s.exchange] * 100).toFixed(1);
887
+ const currentPct = totalEquity > 0 ? ((s.equity / totalEquity) * 100).toFixed(1) : "0.0";
888
+ const targetUsd = totalAvailable * weights[s.exchange];
889
+ const diff = s.available - targetUsd;
890
+ const diffStr = diff >= 0
891
+ ? chalk.green(`+$${formatUsd(diff)}`)
892
+ : chalk.red(`-$${formatUsd(Math.abs(diff))}`);
893
+ return [
894
+ chalk.white.bold(exAbbr(s.exchange)),
895
+ `$${formatUsd(s.available)}`,
896
+ `${currentPct}%`,
897
+ `$${formatUsd(targetUsd)}`,
898
+ `${targetPct}%`,
899
+ diffStr,
900
+ ];
901
+ });
902
+ console.log(makeTable(["Exch", "Available", "Current%", "Target$", "Target%", "Diff"], stateRows));
903
+ // Show moves
904
+ console.log(chalk.cyan.bold("\n Transfers\n"));
905
+ for (let i = 0; i < moveDetails.length; i++) {
906
+ const m = moveDetails[i];
907
+ console.log(chalk.white.bold(` Move ${i + 1}: $${m.amount} ${exAbbr(m.from)} -> ${exAbbr(m.to)}`));
908
+ console.log(chalk.gray(` Route: ${m.srcChain} -> ${m.dstChain} via ${m.bridgeProvider}`));
909
+ console.log(chalk.gray(` Fee: ~$${m.bridgeFee.toFixed(2)} | Time: ${m.bridgeTime}`));
910
+ console.log();
911
+ }
912
+ const totalFees = moveDetails.reduce((s, m) => s + m.bridgeFee, 0);
913
+ console.log(chalk.gray(` Total bridge fees: ~$${totalFees.toFixed(2)}`));
914
+ if (opts.dryRun) {
915
+ console.log(chalk.yellow("\n [DRY RUN] No transfers executed.\n"));
916
+ }
917
+ else {
918
+ console.log(chalk.yellow("\n To execute, use: perp rebalance execute --auto-bridge\n"));
919
+ }
920
+ });
921
+ }