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,327 @@
1
+ import { printJson, jsonOk, jsonError, makeTable, formatUsd } from "../utils.js";
2
+ import { getHistoricalRates } from "../funding-history.js";
3
+ import chalk from "chalk";
4
+ export function registerBacktestCommands(program, isJson) {
5
+ const backtest = program.command("backtest").description("Backtest trading strategies on historical data");
6
+ // ── Funding Arbitrage Backtest ──
7
+ backtest
8
+ .command("funding-arb")
9
+ .description("Backtest funding rate arbitrage strategy")
10
+ .requiredOption("--symbol <sym>", "Symbol to backtest (e.g., BTC)")
11
+ .option("--days <n>", "Number of days to backtest", "30")
12
+ .option("--spread-entry <pct>", "Annual spread % to enter (default: 10)", "10")
13
+ .option("--spread-close <pct>", "Annual spread % to close (default: 5)", "5")
14
+ .option("--exchanges <list>", "Comma-separated exchange pair (e.g., hyperliquid,pacifica)", "hyperliquid,pacifica")
15
+ .option("--size-usd <usd>", "Position size in USD per leg", "1000")
16
+ .action(async (opts) => {
17
+ const sym = opts.symbol.toUpperCase();
18
+ const days = parseInt(opts.days);
19
+ const spreadEntry = parseFloat(opts.spreadEntry);
20
+ const spreadClose = parseFloat(opts.spreadClose);
21
+ const sizeUsd = parseFloat(opts.sizeUsd);
22
+ const [exchA, exchB] = opts.exchanges.split(",").map(e => e.trim().toLowerCase());
23
+ if (!exchA || !exchB) {
24
+ if (isJson())
25
+ return printJson(jsonError("INVALID_PARAMS", "Need exactly 2 exchanges (e.g., --exchanges hyperliquid,pacifica)"));
26
+ console.error(chalk.red("Error: Need exactly 2 exchanges (e.g., --exchanges hyperliquid,pacifica)"));
27
+ process.exit(1);
28
+ }
29
+ const endTime = new Date();
30
+ const startTime = new Date(endTime.getTime() - days * 24 * 60 * 60 * 1000);
31
+ // Get historical funding data
32
+ const ratesA = getHistoricalRates(sym, exchA, startTime, endTime);
33
+ const ratesB = getHistoricalRates(sym, exchB, startTime, endTime);
34
+ if (ratesA.length === 0 && ratesB.length === 0) {
35
+ if (isJson())
36
+ return printJson(jsonError("NO_DATA", `No historical funding data for ${sym}. Run 'perp funding snapshot' first to collect data.`));
37
+ console.log(chalk.yellow(`\n No historical funding data for ${sym} on ${exchA}/${exchB}.`));
38
+ console.log(chalk.yellow(` Run 'perp funding snapshot' periodically to collect data first.\n`));
39
+ return;
40
+ }
41
+ // Build time-aligned rate pairs
42
+ const rateMap = new Map();
43
+ for (const r of ratesA) {
44
+ // Round timestamp to nearest hour for alignment
45
+ const hourKey = new Date(r.ts).toISOString().slice(0, 13);
46
+ if (!rateMap.has(hourKey))
47
+ rateMap.set(hourKey, {});
48
+ rateMap.get(hourKey).a = r.hourlyRate;
49
+ }
50
+ for (const r of ratesB) {
51
+ const hourKey = new Date(r.ts).toISOString().slice(0, 13);
52
+ if (!rateMap.has(hourKey))
53
+ rateMap.set(hourKey, {});
54
+ rateMap.get(hourKey).b = r.hourlyRate;
55
+ }
56
+ // Sort by time
57
+ const sortedKeys = [...rateMap.keys()].sort();
58
+ // Simulate
59
+ let inPosition = false;
60
+ let entrySpread = 0;
61
+ let entryTime = "";
62
+ let totalTrades = 0;
63
+ let totalFundingCollected = 0;
64
+ let totalHoldingHours = 0;
65
+ const trades = [];
66
+ for (const key of sortedKeys) {
67
+ const pair = rateMap.get(key);
68
+ if (pair.a === undefined || pair.b === undefined)
69
+ continue;
70
+ // Annual spread = |rateA - rateB| * 8760 * 100
71
+ const hourlySpread = Math.abs(pair.a - pair.b);
72
+ const annualSpreadPct = hourlySpread * 8760 * 100;
73
+ if (!inPosition && annualSpreadPct >= spreadEntry) {
74
+ inPosition = true;
75
+ entrySpread = annualSpreadPct;
76
+ entryTime = key;
77
+ }
78
+ else if (inPosition && annualSpreadPct < spreadClose) {
79
+ // Close position
80
+ const holdingHours = (new Date(key).getTime() - new Date(entryTime).getTime()) / (1000 * 60 * 60);
81
+ // Funding collected = sum of hourly spreads during holding period
82
+ let fundingCollected = 0;
83
+ for (const hk of sortedKeys) {
84
+ if (hk >= entryTime && hk <= key) {
85
+ const p = rateMap.get(hk);
86
+ if (p.a !== undefined && p.b !== undefined) {
87
+ fundingCollected += Math.abs(p.a - p.b) * sizeUsd;
88
+ }
89
+ }
90
+ }
91
+ trades.push({
92
+ entryTime,
93
+ exitTime: key,
94
+ holdingHours,
95
+ fundingCollected,
96
+ spread: entrySpread,
97
+ });
98
+ totalTrades++;
99
+ totalFundingCollected += fundingCollected;
100
+ totalHoldingHours += holdingHours;
101
+ inPosition = false;
102
+ }
103
+ }
104
+ // Summary
105
+ const avgHoldingHours = totalTrades > 0 ? totalHoldingHours / totalTrades : 0;
106
+ // Rough PnL estimate: funding collected minus estimated trading costs (0.1% per trade * 2 legs * 2 trades)
107
+ const tradingCosts = totalTrades * 2 * 2 * sizeUsd * 0.001;
108
+ const netPnl = totalFundingCollected - tradingCosts;
109
+ const summary = {
110
+ symbol: sym,
111
+ exchanges: `${exchA} vs ${exchB}`,
112
+ period: `${days} days`,
113
+ dataPoints: sortedKeys.length,
114
+ spreadEntryThreshold: `${spreadEntry}%`,
115
+ spreadCloseThreshold: `${spreadClose}%`,
116
+ sizeUsd,
117
+ totalTrades,
118
+ avgHoldingPeriod: `${avgHoldingHours.toFixed(1)}h`,
119
+ totalFundingCollected: `$${totalFundingCollected.toFixed(2)}`,
120
+ tradingCosts: `$${tradingCosts.toFixed(2)}`,
121
+ netPnl: `$${netPnl.toFixed(2)}`,
122
+ trades,
123
+ };
124
+ if (isJson())
125
+ return printJson(jsonOk(summary));
126
+ console.log(chalk.cyan.bold(`\n Funding Arb Backtest — ${sym}\n`));
127
+ console.log(` Exchanges: ${exchA} vs ${exchB}`);
128
+ console.log(` Period: ${days} days (${sortedKeys.length} data points)`);
129
+ console.log(` Entry spread: >= ${spreadEntry}% annualized`);
130
+ console.log(` Close spread: < ${spreadClose}% annualized`);
131
+ console.log(` Size per leg: $${formatUsd(String(sizeUsd))}`);
132
+ console.log();
133
+ console.log(chalk.white.bold(` Results:`));
134
+ console.log(` Total trades: ${totalTrades}`);
135
+ console.log(` Avg holding period: ${avgHoldingHours.toFixed(1)}h`);
136
+ console.log(` Funding collected: ${chalk.green(`$${totalFundingCollected.toFixed(2)}`)}`);
137
+ console.log(` Trading costs: ${chalk.red(`$${tradingCosts.toFixed(2)}`)}`);
138
+ const pnlColor = netPnl >= 0 ? chalk.green : chalk.red;
139
+ console.log(` Net PnL: ${pnlColor(`$${netPnl.toFixed(2)}`)}`);
140
+ if (trades.length > 0) {
141
+ console.log(chalk.white.bold(`\n Trade History:`));
142
+ const rows = trades.map((t, i) => [
143
+ String(i + 1),
144
+ t.entryTime.replace("T", " "),
145
+ t.exitTime.replace("T", " "),
146
+ `${t.holdingHours.toFixed(1)}h`,
147
+ `${t.spread.toFixed(1)}%`,
148
+ `$${t.fundingCollected.toFixed(2)}`,
149
+ ]);
150
+ console.log(makeTable(["#", "Entry", "Exit", "Duration", "Spread", "Funding"], rows));
151
+ }
152
+ console.log();
153
+ });
154
+ // ── Grid Backtest ──
155
+ backtest
156
+ .command("grid")
157
+ .description("Backtest grid trading strategy on historical klines")
158
+ .requiredOption("--symbol <sym>", "Symbol to backtest (e.g., ETH)")
159
+ .requiredOption("--upper <price>", "Upper price bound")
160
+ .requiredOption("--lower <price>", "Lower price bound")
161
+ .option("--grids <n>", "Number of grid lines", "10")
162
+ .option("--days <n>", "Number of days to backtest", "7")
163
+ .option("--size <base>", "Size per grid in base currency", "0.1")
164
+ .action(async (opts) => {
165
+ const sym = opts.symbol.toUpperCase();
166
+ const upperPrice = parseFloat(opts.upper);
167
+ const lowerPrice = parseFloat(opts.lower);
168
+ const grids = parseInt(opts.grids);
169
+ const days = parseInt(opts.days);
170
+ const sizePerGrid = parseFloat(opts.size);
171
+ if (upperPrice <= lowerPrice) {
172
+ if (isJson())
173
+ return printJson(jsonError("INVALID_PARAMS", "Upper price must be greater than lower price"));
174
+ console.error(chalk.red("Error: Upper price must be greater than lower price"));
175
+ process.exit(1);
176
+ }
177
+ if (grids < 2) {
178
+ if (isJson())
179
+ return printJson(jsonError("INVALID_PARAMS", "Need at least 2 grid lines"));
180
+ console.error(chalk.red("Error: Need at least 2 grid lines"));
181
+ process.exit(1);
182
+ }
183
+ const endTime = Date.now();
184
+ const startTime = endTime - days * 24 * 60 * 60 * 1000;
185
+ // Fetch historical klines from Hyperliquid
186
+ if (!isJson()) {
187
+ console.log(chalk.gray(`\n Fetching ${days}d of 1h klines for ${sym}...`));
188
+ }
189
+ let klines;
190
+ try {
191
+ const resp = await fetch("https://api.hyperliquid.xyz/info", {
192
+ method: "POST",
193
+ headers: { "Content-Type": "application/json" },
194
+ body: JSON.stringify({
195
+ type: "candleSnapshot",
196
+ req: {
197
+ coin: sym,
198
+ interval: "1h",
199
+ startTime,
200
+ endTime,
201
+ },
202
+ }),
203
+ });
204
+ if (!resp.ok)
205
+ throw new Error(`HTTP ${resp.status}`);
206
+ const data = await resp.json();
207
+ klines = data.map(k => ({ t: k.t, o: k.o, h: k.h, l: k.l, c: k.c }));
208
+ }
209
+ catch (err) {
210
+ const msg = err instanceof Error ? err.message : String(err);
211
+ if (isJson())
212
+ return printJson(jsonError("FETCH_ERROR", `Failed to fetch klines: ${msg}`));
213
+ console.error(chalk.red(`Error fetching klines: ${msg}`));
214
+ process.exit(1);
215
+ }
216
+ if (klines.length === 0) {
217
+ if (isJson())
218
+ return printJson(jsonError("NO_DATA", `No kline data for ${sym}`));
219
+ console.log(chalk.yellow(`\n No kline data available for ${sym}.\n`));
220
+ return;
221
+ }
222
+ // Build grid levels
223
+ const step = (upperPrice - lowerPrice) / (grids - 1);
224
+ const gridLevels = [];
225
+ for (let i = 0; i < grids; i++) {
226
+ gridLevels.push(lowerPrice + step * i);
227
+ }
228
+ // Simulate grid fills
229
+ // Track which grid levels have pending orders (buy below current, sell above)
230
+ let totalTrades = 0;
231
+ let totalProfit = 0;
232
+ let maxDrawdown = 0;
233
+ let currentDrawdown = 0;
234
+ let peakProfit = 0;
235
+ const filledBuys = new Set(); // grid indices that have been bought
236
+ // Initialize: determine initial grid state based on first kline
237
+ const firstPrice = parseFloat(klines[0].c);
238
+ for (let i = 0; i < gridLevels.length; i++) {
239
+ if (gridLevels[i] < firstPrice) {
240
+ // Below current price: place buy orders (unfilled)
241
+ }
242
+ else {
243
+ // Above current price: assume we've "bought" these to sell
244
+ filledBuys.add(i);
245
+ }
246
+ }
247
+ for (const kline of klines) {
248
+ const low = parseFloat(kline.l);
249
+ const high = parseFloat(kline.h);
250
+ // Check buy fills (price dipped to grid level)
251
+ for (let i = 0; i < gridLevels.length; i++) {
252
+ if (!filledBuys.has(i) && low <= gridLevels[i]) {
253
+ filledBuys.add(i);
254
+ totalTrades++;
255
+ // Bought at grid level
256
+ }
257
+ }
258
+ // Check sell fills (price rose to grid level)
259
+ for (let i = 0; i < gridLevels.length; i++) {
260
+ if (filledBuys.has(i) && high >= gridLevels[i] && i > 0) {
261
+ // Check if there's a higher grid to sell at
262
+ const sellIdx = i;
263
+ // Find next unfilled buy below to pair with
264
+ for (let j = sellIdx - 1; j >= 0; j--) {
265
+ if (filledBuys.has(j))
266
+ continue;
267
+ // Grid profit = sell level - buy level
268
+ break;
269
+ }
270
+ // Simple model: profit is one step worth
271
+ if (filledBuys.has(i)) {
272
+ filledBuys.delete(i);
273
+ totalTrades++;
274
+ totalProfit += step * sizePerGrid;
275
+ }
276
+ }
277
+ }
278
+ // Track drawdown
279
+ if (totalProfit > peakProfit)
280
+ peakProfit = totalProfit;
281
+ currentDrawdown = peakProfit - totalProfit;
282
+ if (currentDrawdown > maxDrawdown)
283
+ maxDrawdown = currentDrawdown;
284
+ }
285
+ // Calculate some stats
286
+ const lastPrice = parseFloat(klines[klines.length - 1].c);
287
+ const priceRange = `$${formatUsd(String(Math.min(...klines.map(k => parseFloat(k.l)))))} - $${formatUsd(String(Math.max(...klines.map(k => parseFloat(k.h)))))}`;
288
+ const tradingFees = totalTrades * sizePerGrid * lastPrice * 0.00035; // ~0.035% per trade
289
+ const netProfit = totalProfit - tradingFees;
290
+ const summary = {
291
+ symbol: sym,
292
+ period: `${days} days`,
293
+ klines: klines.length,
294
+ priceRange,
295
+ gridRange: `$${formatUsd(String(lowerPrice))} - $${formatUsd(String(upperPrice))}`,
296
+ grids,
297
+ step: `$${step.toFixed(2)}`,
298
+ sizePerGrid,
299
+ totalTrades,
300
+ grossProfit: `$${totalProfit.toFixed(2)}`,
301
+ tradingFees: `$${tradingFees.toFixed(2)}`,
302
+ netProfit: `$${netProfit.toFixed(2)}`,
303
+ maxDrawdown: `$${maxDrawdown.toFixed(2)}`,
304
+ profitPerTrade: totalTrades > 0 ? `$${(netProfit / totalTrades).toFixed(2)}` : "$0.00",
305
+ };
306
+ if (isJson())
307
+ return printJson(jsonOk(summary));
308
+ console.log(chalk.cyan.bold(`\n Grid Backtest — ${sym}\n`));
309
+ console.log(` Period: ${days} days (${klines.length} candles)`);
310
+ console.log(` Price range: ${priceRange}`);
311
+ console.log(` Grid range: $${formatUsd(String(lowerPrice))} - $${formatUsd(String(upperPrice))}`);
312
+ console.log(` Grid lines: ${grids} (step: $${step.toFixed(2)})`);
313
+ console.log(` Size per grid: ${sizePerGrid}`);
314
+ console.log();
315
+ console.log(chalk.white.bold(` Results:`));
316
+ console.log(` Total trades: ${totalTrades}`);
317
+ console.log(` Gross profit: ${chalk.green(`$${totalProfit.toFixed(2)}`)}`);
318
+ console.log(` Trading fees: ${chalk.red(`$${tradingFees.toFixed(2)}`)}`);
319
+ const pnlColor = netProfit >= 0 ? chalk.green : chalk.red;
320
+ console.log(` Net profit: ${pnlColor(`$${netProfit.toFixed(2)}`)}`);
321
+ console.log(` Max drawdown: ${chalk.red(`$${maxDrawdown.toFixed(2)}`)}`);
322
+ if (totalTrades > 0) {
323
+ console.log(` Avg profit/trade: $${(netProfit / totalTrades).toFixed(2)}`);
324
+ }
325
+ console.log();
326
+ });
327
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ export declare function registerBotCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, getAdapterFor: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;