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,519 @@
1
+ import { getMarketSnapshot, evaluateAllConditions } from "./conditions.js";
2
+ import { updateJobState } from "../jobs.js";
3
+ import { checkArbLiquidity } from "../liquidity.js";
4
+ import { computeMatchedSize, reconcileArbFills } from "../arb-sizing.js";
5
+ import chalk from "chalk";
6
+ export async function runBot(adapter, config, jobId, log = defaultLog, extraAdapters) {
7
+ const state = {
8
+ phase: "monitoring",
9
+ startTime: Date.now(),
10
+ equity: 0,
11
+ peakEquity: 0,
12
+ dailyPnl: 0,
13
+ fills: 0,
14
+ totalPnl: 0,
15
+ rebalanceCount: 0,
16
+ lastRebalance: 0,
17
+ strategyActive: false,
18
+ gridOrders: new Map(),
19
+ gridUpper: 0,
20
+ gridLower: 0,
21
+ dcaOrdersPlaced: 0,
22
+ dcaLastOrder: 0,
23
+ arbRunning: false,
24
+ arbPositions: 0,
25
+ };
26
+ let running = true;
27
+ const shutdown = () => { running = false; state.phase = "stopped"; };
28
+ process.on("SIGINT", shutdown);
29
+ process.on("SIGTERM", shutdown);
30
+ // Header
31
+ log(chalk.cyan.bold(`\n ╔═══════════════════════════════════════╗`));
32
+ log(chalk.cyan.bold(` ║ ${config.name.padEnd(37)}║`));
33
+ log(chalk.cyan.bold(` ╚═══════════════════════════════════════╝\n`));
34
+ log(` Exchange: ${chalk.white(config.exchange)}`);
35
+ log(` Symbol: ${chalk.white(config.symbol)}`);
36
+ log(` Strategy: ${chalk.white(config.strategy.type)}`);
37
+ log(` Risk: max drawdown $${config.risk.max_drawdown} | max daily loss $${config.risk.max_daily_loss}`);
38
+ log(` Entry: ${config.entry_conditions.map(c => c.type).join(" + ")}`);
39
+ log(` Exit: ${config.exit_conditions.map(c => c.type).join(" | ") || "manual"}`);
40
+ log("");
41
+ // Get initial equity
42
+ try {
43
+ const bal = await adapter.getBalance();
44
+ state.equity = parseFloat(bal.equity);
45
+ state.peakEquity = state.equity;
46
+ log(` Starting equity: $${state.equity.toFixed(2)}`);
47
+ }
48
+ catch {
49
+ log(chalk.yellow(` Could not fetch initial balance`));
50
+ }
51
+ log(chalk.gray(`\n Monitoring conditions... (Ctrl+C to stop)\n`));
52
+ try {
53
+ while (running) {
54
+ const loopStart = Date.now();
55
+ try {
56
+ // Get market data
57
+ const snapshot = await getMarketSnapshot(adapter, config.symbol);
58
+ // Update equity
59
+ try {
60
+ const bal = await adapter.getBalance();
61
+ state.equity = parseFloat(bal.equity);
62
+ if (state.equity > state.peakEquity)
63
+ state.peakEquity = state.equity;
64
+ state.dailyPnl = state.equity - state.peakEquity; // simplified
65
+ }
66
+ catch { /* non-critical */ }
67
+ const context = {
68
+ equity: state.equity,
69
+ startTime: state.startTime,
70
+ peakEquity: state.peakEquity,
71
+ dailyPnl: state.dailyPnl,
72
+ };
73
+ // ── Phase: Monitoring (waiting for entry conditions) ──
74
+ if (state.phase === "monitoring") {
75
+ const shouldEnter = evaluateAllConditions(config.entry_conditions, snapshot, context, "all");
76
+ if (shouldEnter) {
77
+ log(chalk.green(` ✓ Entry conditions met @ $${snapshot.price.toFixed(2)}`));
78
+ state.phase = "entering";
79
+ }
80
+ else {
81
+ logStatus(log, state, snapshot, "waiting");
82
+ }
83
+ }
84
+ // ── Phase: Entering (start strategy) ──
85
+ if (state.phase === "entering") {
86
+ try {
87
+ await startStrategy(adapter, config, state, snapshot, log, extraAdapters);
88
+ state.phase = "running";
89
+ state.strategyActive = true;
90
+ log(chalk.green(` ▶ Strategy started`));
91
+ }
92
+ catch (err) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ log(chalk.red(` ✗ Strategy start failed: ${msg}`));
95
+ state.phase = "monitoring"; // retry
96
+ }
97
+ }
98
+ // ── Phase: Running (manage strategy + check exit) ──
99
+ if (state.phase === "running") {
100
+ // Check exit conditions (any = stop)
101
+ const shouldExit = config.exit_conditions.length > 0
102
+ && evaluateAllConditions(config.exit_conditions, snapshot, context, "any");
103
+ // Check risk limits
104
+ const drawdown = state.peakEquity - state.equity;
105
+ const riskBreached = drawdown > config.risk.max_drawdown;
106
+ if (shouldExit || riskBreached) {
107
+ const reason = riskBreached
108
+ ? `drawdown $${drawdown.toFixed(2)} > limit $${config.risk.max_drawdown}`
109
+ : "exit condition met";
110
+ log(chalk.yellow(` ⚠ Exiting: ${reason}`));
111
+ state.phase = "exiting";
112
+ }
113
+ else {
114
+ // Manage running strategy
115
+ await manageStrategy(adapter, config, state, snapshot, log, extraAdapters);
116
+ logStatus(log, state, snapshot, "running");
117
+ }
118
+ }
119
+ // ── Phase: Exiting (close everything) ──
120
+ if (state.phase === "exiting") {
121
+ await stopStrategy(adapter, config, state, log);
122
+ state.strategyActive = false;
123
+ if (config.risk.pause_after_loss_sec > 0 && state.dailyPnl < 0) {
124
+ log(chalk.yellow(` ⏸ Pausing ${config.risk.pause_after_loss_sec}s after loss`));
125
+ state.phase = "paused";
126
+ await sleep(config.risk.pause_after_loss_sec * 1000);
127
+ state.phase = "monitoring";
128
+ log(chalk.gray(` Resuming monitoring...`));
129
+ }
130
+ else {
131
+ // If exit due to risk breach, stop completely
132
+ log(chalk.red(` ■ Bot stopped.`));
133
+ break;
134
+ }
135
+ }
136
+ // Update job state
137
+ if (jobId) {
138
+ updateJobState(jobId, {
139
+ result: {
140
+ phase: state.phase,
141
+ equity: state.equity,
142
+ peakEquity: state.peakEquity,
143
+ fills: state.fills,
144
+ totalPnl: state.totalPnl,
145
+ rebalances: state.rebalanceCount,
146
+ runtime: Math.floor((Date.now() - state.startTime) / 1000),
147
+ },
148
+ });
149
+ }
150
+ }
151
+ catch (err) {
152
+ const msg = err instanceof Error ? err.message : String(err);
153
+ log(chalk.red(` ✗ Loop error: ${msg}`));
154
+ }
155
+ // Wait for next interval
156
+ const elapsed = Date.now() - loopStart;
157
+ const waitMs = Math.max(0, config.monitor_interval_sec * 1000 - elapsed);
158
+ if (running && waitMs > 0)
159
+ await sleep(waitMs);
160
+ }
161
+ }
162
+ finally {
163
+ process.removeListener("SIGINT", shutdown);
164
+ process.removeListener("SIGTERM", shutdown);
165
+ // Cleanup: cancel any remaining orders
166
+ if (state.strategyActive) {
167
+ log(chalk.gray(` Cleaning up orders...`));
168
+ try {
169
+ await adapter.cancelAllOrders(config.symbol);
170
+ }
171
+ catch { /* best effort */ }
172
+ }
173
+ }
174
+ const runtime = Math.floor((Date.now() - state.startTime) / 1000);
175
+ log(chalk.cyan.bold(`\n Bot "${config.name}" finished`));
176
+ log(` Runtime: ${formatDuration(runtime)} | Fills: ${state.fills} | PnL: $${state.totalPnl.toFixed(2)}`);
177
+ log(` Equity: $${state.equity.toFixed(2)} (peak: $${state.peakEquity.toFixed(2)})\n`);
178
+ if (jobId) {
179
+ updateJobState(jobId, { status: "done" });
180
+ }
181
+ return { fills: state.fills, totalPnl: state.totalPnl, runtime };
182
+ }
183
+ // ── Strategy lifecycle ──
184
+ async function startStrategy(adapter, config, state, snapshot, log, extraAdapters) {
185
+ const strat = config.strategy;
186
+ if (strat.type === "grid") {
187
+ await startGrid(adapter, config.symbol, strat, state, snapshot, log);
188
+ }
189
+ else if (strat.type === "dca") {
190
+ state.dcaOrdersPlaced = 0;
191
+ state.dcaLastOrder = 0;
192
+ log(` [DCA] Ready: ${strat.amount} ${config.symbol} every ${strat.interval_sec}s`);
193
+ }
194
+ else if (strat.type === "funding-arb") {
195
+ state.arbRunning = true;
196
+ state.arbPositions = 0;
197
+ log(` [ARB] Funding arb ready | spread >= ${strat.min_spread}% | size $${strat.size_usd}`);
198
+ log(` [ARB] Exchanges: ${strat.exchanges.join(", ")}`);
199
+ }
200
+ }
201
+ async function manageStrategy(adapter, config, state, snapshot, log, extraAdapters) {
202
+ const strat = config.strategy;
203
+ if (strat.type === "grid") {
204
+ await manageGrid(adapter, config.symbol, strat, state, snapshot, log);
205
+ }
206
+ else if (strat.type === "dca") {
207
+ await manageDCA(adapter, config.symbol, strat, state, snapshot, log);
208
+ }
209
+ else if (strat.type === "funding-arb") {
210
+ await manageFundingArb(adapter, config.symbol, strat, state, snapshot, log, extraAdapters);
211
+ }
212
+ }
213
+ async function stopStrategy(adapter, config, state, log) {
214
+ log(` Cancelling all orders...`);
215
+ try {
216
+ await adapter.cancelAllOrders(config.symbol);
217
+ }
218
+ catch { /* best effort */ }
219
+ state.gridOrders.clear();
220
+ }
221
+ // ── Grid strategy ──
222
+ async function startGrid(adapter, symbol, params, state, snapshot, log) {
223
+ // Determine price range
224
+ if (params.range_mode === "auto") {
225
+ const pct = params.range_pct ?? 3;
226
+ state.gridUpper = snapshot.price * (1 + pct / 100);
227
+ state.gridLower = snapshot.price * (1 - pct / 100);
228
+ log(` [GRID] Auto range: $${state.gridLower.toFixed(2)} - $${state.gridUpper.toFixed(2)} (±${pct}%)`);
229
+ }
230
+ else {
231
+ if (!params.upper || !params.lower)
232
+ throw new Error("Fixed grid requires upper and lower");
233
+ state.gridUpper = params.upper;
234
+ state.gridLower = params.lower;
235
+ }
236
+ // Set leverage
237
+ if (params.leverage) {
238
+ try {
239
+ await adapter.setLeverage(symbol, params.leverage);
240
+ log(` [GRID] Leverage: ${params.leverage}x`);
241
+ }
242
+ catch { /* non-critical */ }
243
+ }
244
+ // Place grid orders
245
+ await placeGridOrders(adapter, symbol, params, state, snapshot.price, log);
246
+ }
247
+ async function placeGridOrders(adapter, symbol, params, state, currentPrice, log) {
248
+ const step = (state.gridUpper - state.gridLower) / (params.grids - 1);
249
+ const sizePerGrid = params.size / params.grids;
250
+ let placed = 0;
251
+ // Cancel existing orders first
252
+ if (state.gridOrders.size > 0) {
253
+ try {
254
+ await adapter.cancelAllOrders(symbol);
255
+ }
256
+ catch { /* */ }
257
+ state.gridOrders.clear();
258
+ }
259
+ for (let i = 0; i < params.grids; i++) {
260
+ const price = state.gridLower + step * i;
261
+ // Determine side based on current price
262
+ let side;
263
+ if (params.side === "long")
264
+ side = "buy";
265
+ else if (params.side === "short")
266
+ side = "sell";
267
+ else
268
+ side = price < currentPrice ? "buy" : "sell";
269
+ try {
270
+ const result = await adapter.limitOrder(symbol, side, price.toFixed(2), String(sizePerGrid));
271
+ const orderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
272
+ state.gridOrders.set(i, orderId);
273
+ placed++;
274
+ }
275
+ catch {
276
+ // skip failed grid line
277
+ }
278
+ }
279
+ log(` [GRID] Placed ${placed}/${params.grids} orders (step: $${step.toFixed(2)}, size: ${sizePerGrid.toFixed(6)})`);
280
+ }
281
+ async function manageGrid(adapter, symbol, params, state, snapshot, log) {
282
+ const step = (state.gridUpper - state.gridLower) / (params.grids - 1);
283
+ const sizePerGrid = params.size / params.grids;
284
+ // Check for fills
285
+ try {
286
+ const openOrders = await adapter.getOpenOrders();
287
+ const openIds = new Set(openOrders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase()).map(o => o.orderId));
288
+ let newFills = 0;
289
+ for (const [idx, orderId] of state.gridOrders.entries()) {
290
+ if (!openIds.has(orderId)) {
291
+ // Order filled — place opposite order
292
+ newFills++;
293
+ state.fills++;
294
+ state.totalPnl += step * sizePerGrid;
295
+ // Determine new side and price
296
+ const oldPrice = state.gridLower + step * idx;
297
+ const wasBuy = oldPrice < snapshot.price;
298
+ const newSide = wasBuy ? "sell" : "buy";
299
+ const newPrice = wasBuy ? oldPrice + step : oldPrice - step;
300
+ if (newPrice >= state.gridLower && newPrice <= state.gridUpper) {
301
+ try {
302
+ const result = await adapter.limitOrder(symbol, newSide, newPrice.toFixed(2), String(sizePerGrid));
303
+ const newOrderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
304
+ state.gridOrders.set(idx, newOrderId);
305
+ }
306
+ catch { /* skip */ }
307
+ }
308
+ else {
309
+ state.gridOrders.delete(idx);
310
+ }
311
+ }
312
+ }
313
+ if (newFills > 0) {
314
+ log(chalk.green(` [GRID] ${newFills} fill(s) | Total: ${state.fills} | Est. PnL: $${state.totalPnl.toFixed(2)}`));
315
+ }
316
+ }
317
+ catch { /* retry next loop */ }
318
+ // Auto-rebalance if price exits range
319
+ if (params.rebalance) {
320
+ const outOfRange = snapshot.price > state.gridUpper || snapshot.price < state.gridLower;
321
+ const cooldownOk = Date.now() - state.lastRebalance > params.rebalance_cooldown * 1000;
322
+ if (outOfRange && cooldownOk) {
323
+ const pct = params.range_pct ?? 3;
324
+ state.gridUpper = snapshot.price * (1 + pct / 100);
325
+ state.gridLower = snapshot.price * (1 - pct / 100);
326
+ state.rebalanceCount++;
327
+ state.lastRebalance = Date.now();
328
+ log(chalk.yellow(` [GRID] Rebalance #${state.rebalanceCount}: price $${snapshot.price.toFixed(2)} outside range → new $${state.gridLower.toFixed(2)} - $${state.gridUpper.toFixed(2)}`));
329
+ await placeGridOrders(adapter, symbol, params, state, snapshot.price, log);
330
+ }
331
+ }
332
+ }
333
+ // ── DCA strategy ──
334
+ async function manageDCA(adapter, symbol, params, state, snapshot, log) {
335
+ // Check if it's time for next order
336
+ const timeSinceLast = (Date.now() - state.dcaLastOrder) / 1000;
337
+ if (state.dcaLastOrder > 0 && timeSinceLast < params.interval_sec)
338
+ return;
339
+ // Check order limit
340
+ if (params.total_orders > 0 && state.dcaOrdersPlaced >= params.total_orders)
341
+ return;
342
+ // Check price limit
343
+ if (params.price_limit) {
344
+ // For DCA, we assume buying — skip if price above limit
345
+ if (snapshot.price > params.price_limit) {
346
+ return;
347
+ }
348
+ }
349
+ // Place market order
350
+ try {
351
+ const side = "buy"; // DCA is typically buying
352
+ await adapter.marketOrder(symbol, side, String(params.amount));
353
+ state.dcaOrdersPlaced++;
354
+ state.dcaLastOrder = Date.now();
355
+ state.fills++;
356
+ const progress = params.total_orders > 0 ? ` (${state.dcaOrdersPlaced}/${params.total_orders})` : "";
357
+ log(chalk.green(` [DCA] Order #${state.dcaOrdersPlaced}${progress}: buy ${params.amount} ${symbol} @ $${snapshot.price.toFixed(2)}`));
358
+ }
359
+ catch (err) {
360
+ const msg = err instanceof Error ? err.message : String(err);
361
+ log(chalk.red(` [DCA] Order failed: ${msg}`));
362
+ }
363
+ }
364
+ async function fetchRatesFromAdapter(adapter, exchangeName) {
365
+ try {
366
+ const markets = await adapter.getMarkets();
367
+ return markets.map(m => ({
368
+ symbol: m.symbol,
369
+ rate: parseFloat(m.fundingRate),
370
+ price: parseFloat(m.markPrice),
371
+ }));
372
+ }
373
+ catch {
374
+ return [];
375
+ }
376
+ }
377
+ async function manageFundingArb(adapter, symbol, params, state, _snapshot, log, extraAdapters) {
378
+ // Build adapter map from primary + extras
379
+ const adapters = new Map();
380
+ adapters.set(adapter.name.toLowerCase(), adapter);
381
+ if (extraAdapters) {
382
+ for (const [name, a] of extraAdapters)
383
+ adapters.set(name, a);
384
+ }
385
+ if (adapters.size < 2) {
386
+ log(chalk.yellow(` [ARB] Need 2+ exchanges, have ${adapters.size}. Skipping.`));
387
+ return;
388
+ }
389
+ // Fetch rates from all exchanges
390
+ const ratesByExchange = new Map();
391
+ for (const [name, a] of adapters) {
392
+ const rates = await fetchRatesFromAdapter(a, name);
393
+ const map = new Map();
394
+ for (const r of rates)
395
+ map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
396
+ ratesByExchange.set(name, map);
397
+ }
398
+ // Find opportunities: largest spread between any two exchanges
399
+ const exchangeNames = [...ratesByExchange.keys()];
400
+ const opportunities = [];
401
+ // Collect all symbols
402
+ const allSymbols = new Set();
403
+ for (const [, map] of ratesByExchange) {
404
+ for (const sym of map.keys())
405
+ allSymbols.add(sym);
406
+ }
407
+ for (const sym of allSymbols) {
408
+ let minRate = Infinity, maxRate = -Infinity;
409
+ let minExchange = "", maxExchange = "";
410
+ for (const exName of exchangeNames) {
411
+ const rate = ratesByExchange.get(exName)?.get(sym)?.rate;
412
+ if (rate === undefined)
413
+ continue;
414
+ if (rate < minRate) {
415
+ minRate = rate;
416
+ minExchange = exName;
417
+ }
418
+ if (rate > maxRate) {
419
+ maxRate = rate;
420
+ maxExchange = exName;
421
+ }
422
+ }
423
+ if (minExchange && maxExchange && minExchange !== maxExchange) {
424
+ const { computeAnnualSpread: cas } = await import("../funding.js");
425
+ const spread = cas(maxRate, maxExchange, minRate, minExchange);
426
+ if (spread >= params.min_spread) {
427
+ opportunities.push({
428
+ symbol: sym,
429
+ longExchange: minExchange, // long where rate is low (we receive funding)
430
+ shortExchange: maxExchange, // short where rate is high (we pay less or receive)
431
+ longRate: minRate,
432
+ shortRate: maxRate,
433
+ spread,
434
+ });
435
+ }
436
+ }
437
+ }
438
+ // Sort by spread descending
439
+ opportunities.sort((a, b) => b.spread - a.spread);
440
+ if (opportunities.length > 0) {
441
+ const top = opportunities.slice(0, 3);
442
+ for (const opp of top) {
443
+ log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread (long ${opp.longExchange} ${(opp.longRate * 100).toFixed(4)}% / short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
444
+ }
445
+ // Auto-execute if under position limit
446
+ if (state.arbPositions < params.max_positions && opportunities.length > 0) {
447
+ const best = opportunities[0];
448
+ const longAdapter = adapters.get(best.longExchange);
449
+ const shortAdapter = adapters.get(best.shortExchange);
450
+ if (longAdapter && shortAdapter) {
451
+ // Calculate size from USD
452
+ const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
453
+ if (price > 0) {
454
+ // Liquidity check & size adjustment
455
+ const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, params.size_usd, 0.5, (msg) => log(chalk.yellow(` ${msg}`)));
456
+ if (!liq.viable)
457
+ return;
458
+ // Compute matched size for both legs
459
+ const matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
460
+ if (!matched) {
461
+ log(chalk.yellow(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`));
462
+ return;
463
+ }
464
+ try {
465
+ log(chalk.cyan(` [ARB] Opening: ${matched.size} ${best.symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)`));
466
+ await Promise.all([
467
+ longAdapter.marketOrder(best.symbol, "buy", matched.size),
468
+ shortAdapter.marketOrder(best.symbol, "sell", matched.size),
469
+ ]);
470
+ // Verify fills match, correct if needed
471
+ try {
472
+ const recon = await reconcileArbFills(longAdapter, shortAdapter, best.symbol, (msg) => log(chalk.yellow(` ${msg}`)));
473
+ if (!recon.matched) {
474
+ log(chalk.red(` [ARB] WARNING: fills not matched after correction attempt`));
475
+ }
476
+ }
477
+ catch { /* non-critical */ }
478
+ state.arbPositions++;
479
+ state.fills += 2;
480
+ log(chalk.green(` [ARB] Position opened! (${state.arbPositions}/${params.max_positions})`));
481
+ }
482
+ catch (err) {
483
+ const msg = err instanceof Error ? err.message : String(err);
484
+ log(chalk.red(` [ARB] Execution failed: ${msg}`));
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ else {
491
+ log(chalk.gray(` [ARB] No opportunities >= ${params.min_spread}% spread`));
492
+ }
493
+ }
494
+ // ── Utils ──
495
+ function logStatus(log, state, snapshot, phase) {
496
+ const runtime = formatDuration(Math.floor((Date.now() - state.startTime) / 1000));
497
+ const vol = snapshot.volatility24h.toFixed(1);
498
+ const fr = (snapshot.fundingRate * 100).toFixed(4);
499
+ const eq = state.equity.toFixed(2);
500
+ const dd = (state.peakEquity - state.equity).toFixed(2);
501
+ log(chalk.gray(` [${phase}] $${snapshot.price.toFixed(2)} | vol: ${vol}% | fr: ${fr}% | ` +
502
+ `eq: $${eq} | dd: $${dd} | fills: ${state.fills} | ${runtime}`));
503
+ }
504
+ function defaultLog(msg) {
505
+ const ts = new Date().toLocaleTimeString();
506
+ console.log(`${chalk.gray(ts)} ${msg}`);
507
+ }
508
+ function formatDuration(sec) {
509
+ if (sec < 60)
510
+ return `${sec}s`;
511
+ const m = Math.floor(sec / 60);
512
+ if (m < 60)
513
+ return `${m}m${sec % 60}s`;
514
+ const h = Math.floor(m / 60);
515
+ return `${h}h${m % 60}m`;
516
+ }
517
+ function sleep(ms) {
518
+ return new Promise(r => setTimeout(r, ms));
519
+ }
@@ -0,0 +1,11 @@
1
+ import type { BotConfig } from "./config.js";
2
+ export interface Preset {
3
+ name: string;
4
+ description: string;
5
+ strategy: "grid" | "dca" | "funding-arb";
6
+ risk: "low" | "medium" | "high";
7
+ buildConfig: (exchange: string, symbol: string) => BotConfig;
8
+ }
9
+ export declare const PRESETS: Preset[];
10
+ export declare function getPreset(name: string): Preset | undefined;
11
+ export declare function getPresetsByStrategy(strategy: string): Preset[];