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,114 @@
1
+ import { updateJobState } from "../jobs.js";
2
+ export async function runDCA(adapter, params, jobId, log = console.log) {
3
+ const { symbol, side, amountPerOrder, intervalSec, totalOrders } = params;
4
+ const maxRuntime = (params.maxRuntime ?? 0) * 1000;
5
+ const state = {
6
+ ordersPlaced: 0,
7
+ totalFilled: 0,
8
+ totalCost: 0,
9
+ avgPrice: 0,
10
+ errors: 0,
11
+ startedAt: Date.now(),
12
+ running: true,
13
+ };
14
+ const target = totalOrders > 0 ? `${totalOrders} orders` : "unlimited (Ctrl+C to stop)";
15
+ log(`[DCA] ${side.toUpperCase()} ${amountPerOrder} ${symbol} every ${intervalSec}s | ${target}`);
16
+ if (params.priceLimit) {
17
+ log(`[DCA] Price limit: $${params.priceLimit} (${side === "buy" ? "won't buy above" : "won't sell below"})`);
18
+ }
19
+ // Graceful shutdown
20
+ const shutdown = () => { state.running = false; };
21
+ process.on("SIGINT", shutdown);
22
+ process.on("SIGTERM", shutdown);
23
+ try {
24
+ while (state.running) {
25
+ // Check if we've reached order limit
26
+ if (totalOrders > 0 && state.ordersPlaced >= totalOrders) {
27
+ log(`[DCA] Reached target of ${totalOrders} orders. Done.`);
28
+ break;
29
+ }
30
+ // Check max runtime
31
+ if (maxRuntime > 0 && Date.now() - state.startedAt > maxRuntime) {
32
+ log(`[DCA] Max runtime reached. Stopping.`);
33
+ break;
34
+ }
35
+ // Check price limit
36
+ if (params.priceLimit) {
37
+ try {
38
+ const markets = await adapter.getMarkets();
39
+ const market = markets.find(m => m.symbol.toUpperCase() === symbol.toUpperCase());
40
+ if (market) {
41
+ const price = parseFloat(market.markPrice);
42
+ if (side === "buy" && price > params.priceLimit) {
43
+ log(`[DCA] Price $${price.toFixed(2)} > limit $${params.priceLimit}. Skipping.`);
44
+ await sleep(intervalSec * 1000);
45
+ continue;
46
+ }
47
+ if (side === "sell" && price < params.priceLimit) {
48
+ log(`[DCA] Price $${price.toFixed(2)} < limit $${params.priceLimit}. Skipping.`);
49
+ await sleep(intervalSec * 1000);
50
+ continue;
51
+ }
52
+ }
53
+ }
54
+ catch {
55
+ // non-critical, proceed with order
56
+ }
57
+ }
58
+ // Place market order
59
+ try {
60
+ const result = await adapter.marketOrder(symbol, side, String(amountPerOrder));
61
+ state.ordersPlaced++;
62
+ state.totalFilled += amountPerOrder;
63
+ const fillPrice = Number(result?.price ?? result?.avg_price ?? result?.fill_price ?? 0);
64
+ if (fillPrice > 0) {
65
+ state.totalCost += amountPerOrder * fillPrice;
66
+ state.avgPrice = state.totalCost / state.totalFilled;
67
+ }
68
+ const progress = totalOrders > 0 ? ` (${state.ordersPlaced}/${totalOrders})` : "";
69
+ log(`[DCA] Order #${state.ordersPlaced}${progress}: ${side} ${amountPerOrder} ${symbol}${state.avgPrice > 0 ? ` @ $${state.avgPrice.toFixed(2)} avg` : ""}`);
70
+ // Update job state
71
+ if (jobId) {
72
+ updateJobState(jobId, {
73
+ result: {
74
+ ordersPlaced: state.ordersPlaced,
75
+ totalFilled: state.totalFilled,
76
+ avgPrice: state.avgPrice,
77
+ errors: state.errors,
78
+ runtime: Math.floor((Date.now() - state.startedAt) / 1000),
79
+ },
80
+ });
81
+ }
82
+ }
83
+ catch (err) {
84
+ state.errors++;
85
+ const msg = err instanceof Error ? err.message : String(err);
86
+ log(`[DCA] Order error: ${msg}`);
87
+ if (state.errors > 10 && state.errors > state.ordersPlaced) {
88
+ log(`[DCA] Too many errors. Stopping.`);
89
+ break;
90
+ }
91
+ }
92
+ // Wait for next interval
93
+ if (state.running && (totalOrders === 0 || state.ordersPlaced < totalOrders)) {
94
+ await sleep(intervalSec * 1000);
95
+ }
96
+ }
97
+ }
98
+ finally {
99
+ process.removeListener("SIGINT", shutdown);
100
+ process.removeListener("SIGTERM", shutdown);
101
+ }
102
+ const runtime = Math.floor((Date.now() - state.startedAt) / 1000);
103
+ log(`[DCA] Done. ${state.ordersPlaced} orders, ${state.totalFilled} filled, avg $${state.avgPrice.toFixed(2)}, ${state.errors} errors, ${runtime}s`);
104
+ if (jobId) {
105
+ updateJobState(jobId, {
106
+ status: "done",
107
+ result: { ordersPlaced: state.ordersPlaced, totalFilled: state.totalFilled, avgPrice: state.avgPrice, runtime },
108
+ });
109
+ }
110
+ return { ordersPlaced: state.ordersPlaced, totalFilled: state.totalFilled, avgPrice: state.avgPrice, runtime };
111
+ }
112
+ function sleep(ms) {
113
+ return new Promise((r) => setTimeout(r, ms));
114
+ }
@@ -0,0 +1,15 @@
1
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
2
+ export interface FundingArbParams {
3
+ minSpread: number;
4
+ closeSpread: number;
5
+ size: string;
6
+ sizeUsd?: number;
7
+ symbols?: string[];
8
+ intervalSec: number;
9
+ autoExecute: boolean;
10
+ maxPositions?: number;
11
+ autoRebalance?: boolean;
12
+ rebalanceThreshold?: number;
13
+ maxDrawdown?: number;
14
+ }
15
+ export declare function runFundingArb(adapters: Map<string, ExchangeAdapter>, params: FundingArbParams, jobId?: string, log?: (msg: string) => void): Promise<void>;
@@ -0,0 +1,281 @@
1
+ import { symbolMatch } from "../utils.js";
2
+ import { updateJobState } from "../jobs.js";
3
+ import { fetchAllBalances, computeRebalancePlan, hasEnoughBalance } from "../rebalance.js";
4
+ import { checkArbLiquidity } from "../liquidity.js";
5
+ import { computeAnnualSpread } from "../funding.js";
6
+ import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../shared-api.js";
7
+ import { computeMatchedSize, reconcileArbFills } from "../arb-sizing.js";
8
+ async function fetchAllRates() {
9
+ const [pacRates, hlRates, ltRates] = await Promise.allSettled([
10
+ fetchPacificaPrices().then(assets => assets.map(p => ({
11
+ exchange: "pacifica", symbol: p.symbol, fundingRate: p.funding, markPrice: p.mark,
12
+ }))),
13
+ fetchHyperliquidMeta().then(assets => assets.map(a => ({
14
+ exchange: "hyperliquid", symbol: a.symbol, fundingRate: a.funding, markPrice: a.markPx,
15
+ }))),
16
+ (async () => {
17
+ const [details, funding] = await Promise.all([
18
+ fetchLighterOrderBookDetails(),
19
+ fetchLighterFundingRates(),
20
+ ]);
21
+ const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
22
+ const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
23
+ return funding.map(fr => ({
24
+ exchange: "lighter",
25
+ symbol: fr.symbol || symMap.get(fr.marketId) || "",
26
+ fundingRate: fr.rate,
27
+ markPrice: fr.markPrice || priceMap.get(fr.marketId) || 0,
28
+ }));
29
+ })(),
30
+ ]);
31
+ return [
32
+ ...(pacRates.status === "fulfilled" ? pacRates.value : []),
33
+ ...(hlRates.status === "fulfilled" ? hlRates.value : []),
34
+ ...(ltRates.status === "fulfilled" ? ltRates.value : []),
35
+ ];
36
+ }
37
+ // annualize: use computeAnnualSpread from funding.ts for cross-exchange comparison
38
+ export async function runFundingArb(adapters, params, jobId, log = console.log) {
39
+ const maxPos = params.maxPositions ?? 3;
40
+ const closeSpread = params.closeSpread ?? 5;
41
+ const positions = [];
42
+ let cycleCount = 0;
43
+ let lastRebalanceCheck = 0;
44
+ log(`[ARB] Funding rate arbitrage started`);
45
+ log(`[ARB] Entry spread: >= ${params.minSpread}% | Close spread: <= ${closeSpread}%`);
46
+ log(`[ARB] Size: ${params.size} | Interval: ${params.intervalSec}s`);
47
+ log(`[ARB] Auto-execute: ${params.autoExecute} | Max positions: ${maxPos}`);
48
+ log(`[ARB] Exchanges: ${[...adapters.keys()].join(", ")}`);
49
+ if (params.autoRebalance)
50
+ log(`[ARB] Auto-rebalance: ON (threshold: $${params.rebalanceThreshold ?? 100})`);
51
+ if (params.maxDrawdown)
52
+ log(`[ARB] Max drawdown: $${params.maxDrawdown}`);
53
+ if (params.symbols?.length)
54
+ log(`[ARB] Symbols: ${params.symbols.join(", ")}`);
55
+ while (true) {
56
+ cycleCount++;
57
+ try {
58
+ const rates = await fetchAllRates();
59
+ // Group by symbol
60
+ const rateMap = new Map();
61
+ for (const r of rates) {
62
+ if (params.symbols?.length && !params.symbols.includes(r.symbol))
63
+ continue;
64
+ if (!rateMap.has(r.symbol))
65
+ rateMap.set(r.symbol, []);
66
+ rateMap.get(r.symbol).push(r);
67
+ }
68
+ // ── Phase 1: Check positions for close conditions ──
69
+ for (let i = positions.length - 1; i >= 0; i--) {
70
+ const pos = positions[i];
71
+ const symbolRates = rateMap.get(pos.symbol);
72
+ if (!symbolRates)
73
+ continue;
74
+ const longRate = symbolRates.find((r) => r.exchange === pos.longExchange);
75
+ const shortRate = symbolRates.find((r) => r.exchange === pos.shortExchange);
76
+ if (!longRate || !shortRate)
77
+ continue;
78
+ const currentSpread = computeAnnualSpread(shortRate.fundingRate, shortRate.exchange, longRate.fundingRate, longRate.exchange);
79
+ if (currentSpread <= closeSpread) {
80
+ log(`[ARB] CLOSE signal: ${pos.symbol} spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`);
81
+ if (params.autoExecute) {
82
+ const longAdapter = adapters.get(pos.longExchange);
83
+ const shortAdapter = adapters.get(pos.shortExchange);
84
+ if (longAdapter && shortAdapter) {
85
+ try {
86
+ await Promise.all([
87
+ longAdapter.marketOrder(pos.symbol, "sell", pos.size),
88
+ shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
89
+ ]);
90
+ log(`[ARB] CLOSED ${pos.symbol} — both legs unwound`);
91
+ // Estimate P&L from funding collected
92
+ const hoursOpen = (Date.now() - pos.openedAt) / (1000 * 60 * 60);
93
+ // entrySpread is annualized %; convert to per-hour rate
94
+ const hourlySpreadRate = pos.entrySpread / 100 / (24 * 365);
95
+ const estimatedFunding = hourlySpreadRate * hoursOpen * Number(pos.size) * pos.entryPrices.long;
96
+ log(`[ARB] Est. funding P&L: ~$${estimatedFunding.toFixed(2)} (${hoursOpen.toFixed(1)}h open)`);
97
+ }
98
+ catch (err) {
99
+ const msg = err instanceof Error ? err.message : String(err);
100
+ log(`[ARB] Close error: ${msg}`);
101
+ continue; // don't remove position if close failed
102
+ }
103
+ }
104
+ }
105
+ positions.splice(i, 1);
106
+ }
107
+ }
108
+ // ── Phase 2: Max drawdown check ──
109
+ if (params.maxDrawdown && params.autoExecute && positions.length > 0) {
110
+ try {
111
+ const snapshots = await fetchAllBalances(adapters);
112
+ const totalPnl = snapshots.reduce((s, e) => s + e.unrealizedPnl, 0);
113
+ if (totalPnl < -params.maxDrawdown) {
114
+ log(`[ARB] MAX DRAWDOWN hit: uPnL $${totalPnl.toFixed(2)} < -$${params.maxDrawdown}`);
115
+ log(`[ARB] Closing all ${positions.length} positions...`);
116
+ await closeAllPositions(positions, adapters, log);
117
+ positions.length = 0;
118
+ }
119
+ }
120
+ catch { /* non-critical */ }
121
+ }
122
+ // ── Phase 3: Balance check + rebalance trigger ──
123
+ let balanceSnapshots = null;
124
+ const shouldCheckBalance = params.autoRebalance && (Date.now() - lastRebalanceCheck > 300_000); // every 5 min
125
+ if (shouldCheckBalance || (params.autoExecute && positions.length < maxPos)) {
126
+ try {
127
+ balanceSnapshots = await fetchAllBalances(adapters);
128
+ lastRebalanceCheck = Date.now();
129
+ if (params.autoRebalance) {
130
+ const threshold = params.rebalanceThreshold ?? 100;
131
+ const lowExchanges = balanceSnapshots.filter((s) => s.available < threshold);
132
+ if (lowExchanges.length > 0) {
133
+ const plan = computeRebalancePlan(balanceSnapshots, { minMove: 50, reserve: 20 });
134
+ if (plan.moves.length > 0) {
135
+ log(`[ARB] Rebalance needed: ${plan.summary}`);
136
+ for (const move of plan.moves) {
137
+ log(`[ARB] $${move.amount} ${move.from} → ${move.to}`);
138
+ }
139
+ log(`[ARB] Run 'perp rebalance execute' to rebalance.`);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ catch { /* non-critical */ }
145
+ }
146
+ // ── Phase 4: Find new entry opportunities ──
147
+ if (positions.length < maxPos) {
148
+ for (const [symbol, symbolRates] of rateMap) {
149
+ if (symbolRates.length < 2)
150
+ continue;
151
+ if (positions.find((p) => symbolMatch(p.symbol, symbol)))
152
+ continue;
153
+ if (positions.length >= maxPos)
154
+ break;
155
+ // Only consider exchanges we have adapters for
156
+ const available = symbolRates.filter((r) => adapters.has(r.exchange));
157
+ if (available.length < 2)
158
+ continue;
159
+ available.sort((a, b) => a.fundingRate - b.fundingRate);
160
+ const lowest = available[0];
161
+ const highest = available[available.length - 1];
162
+ if (lowest.exchange === highest.exchange)
163
+ continue;
164
+ const annualSpread = computeAnnualSpread(highest.fundingRate, highest.exchange, lowest.fundingRate, lowest.exchange);
165
+ if (annualSpread >= params.minSpread) {
166
+ const longEx = lowest.exchange;
167
+ const shortEx = highest.exchange;
168
+ log(`[ARB] ENTRY signal: ${symbol} spread ${annualSpread.toFixed(1)}% — long ${longEx} (${(lowest.fundingRate * 100).toFixed(4)}%) / short ${shortEx} (${(highest.fundingRate * 100).toFixed(4)}%)`);
169
+ if (params.autoExecute) {
170
+ const longAdapter = adapters.get(longEx);
171
+ const shortAdapter = adapters.get(shortEx);
172
+ if (!longAdapter || !shortAdapter) {
173
+ log(`[ARB] Skip: adapter not available for ${longEx} or ${shortEx}`);
174
+ continue;
175
+ }
176
+ // Check orderbook liquidity & adjust size
177
+ const requestedUsd = params.sizeUsd ?? Number(params.size) * highest.markPrice;
178
+ const liq = await checkArbLiquidity(longAdapter, shortAdapter, symbol, requestedUsd, 0.5, log);
179
+ if (!liq.viable)
180
+ continue;
181
+ // Compute matched size (same for both legs)
182
+ const matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx);
183
+ if (!matched) {
184
+ log(`[ARB] Skip ${symbol}: can't compute matched size (min notional or precision issue)`);
185
+ continue;
186
+ }
187
+ // Check balances before trading
188
+ if (balanceSnapshots) {
189
+ if (!hasEnoughBalance(balanceSnapshots, longEx, matched.notional) ||
190
+ !hasEnoughBalance(balanceSnapshots, shortEx, matched.notional)) {
191
+ log(`[ARB] Skip ${symbol}: insufficient balance on ${longEx} or ${shortEx} (need ~$${matched.notional.toFixed(0)} per leg)`);
192
+ continue;
193
+ }
194
+ }
195
+ try {
196
+ log(`[ARB] Opening: ${matched.size} ${symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
197
+ await Promise.all([
198
+ longAdapter.marketOrder(symbol, "buy", matched.size),
199
+ shortAdapter.marketOrder(symbol, "sell", matched.size),
200
+ ]);
201
+ // Verify fills match, correct if needed
202
+ try {
203
+ const recon = await reconcileArbFills(longAdapter, shortAdapter, symbol, log);
204
+ if (!recon.matched) {
205
+ log(`[ARB] WARNING: fills not matched after correction attempt`);
206
+ }
207
+ }
208
+ catch { /* non-critical */ }
209
+ positions.push({
210
+ symbol,
211
+ longExchange: longEx,
212
+ shortExchange: shortEx,
213
+ size: matched.size,
214
+ entrySpread: annualSpread,
215
+ openedAt: Date.now(),
216
+ entryPrices: { long: lowest.markPrice, short: highest.markPrice },
217
+ });
218
+ log(`[ARB] OPENED ${symbol} delta-neutral (${positions.length}/${maxPos})`);
219
+ }
220
+ catch (err) {
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ log(`[ARB] Entry error: ${msg}`);
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ // ── Update job state ──
229
+ if (jobId) {
230
+ updateJobState(jobId, {
231
+ result: {
232
+ cycle: cycleCount,
233
+ activePositions: positions.length,
234
+ positions: positions.map((p) => ({
235
+ symbol: p.symbol,
236
+ long: p.longExchange,
237
+ short: p.shortExchange,
238
+ size: p.size,
239
+ entrySpread: p.entrySpread,
240
+ hoursOpen: ((Date.now() - p.openedAt) / 3_600_000).toFixed(1),
241
+ })),
242
+ lastCheck: new Date().toISOString(),
243
+ },
244
+ });
245
+ }
246
+ // Periodic status log
247
+ if (cycleCount % 10 === 0) {
248
+ const posInfo = positions.length > 0
249
+ ? positions.map((p) => `${p.symbol}(${p.entrySpread.toFixed(0)}%)`).join(", ")
250
+ : "none";
251
+ log(`[ARB] Cycle ${cycleCount} | ${positions.length} positions: ${posInfo} | ${rateMap.size} symbols`);
252
+ }
253
+ }
254
+ catch (err) {
255
+ const msg = err instanceof Error ? err.message : String(err);
256
+ log(`[ARB] Cycle error: ${msg}`);
257
+ }
258
+ await sleep(params.intervalSec * 1000);
259
+ }
260
+ }
261
+ async function closeAllPositions(positions, adapters, log) {
262
+ for (const pos of positions) {
263
+ const longAdapter = adapters.get(pos.longExchange);
264
+ const shortAdapter = adapters.get(pos.shortExchange);
265
+ if (!longAdapter || !shortAdapter)
266
+ continue;
267
+ try {
268
+ await Promise.all([
269
+ longAdapter.marketOrder(pos.symbol, "sell", pos.size),
270
+ shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
271
+ ]);
272
+ log(`[ARB] Emergency closed ${pos.symbol}`);
273
+ }
274
+ catch (err) {
275
+ log(`[ARB] Failed to close ${pos.symbol}: ${err instanceof Error ? err.message : err}`);
276
+ }
277
+ }
278
+ }
279
+ function sleep(ms) {
280
+ return new Promise((r) => setTimeout(r, ms));
281
+ }
@@ -0,0 +1,34 @@
1
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
2
+ export interface GridParams {
3
+ symbol: string;
4
+ side: "long" | "short" | "neutral";
5
+ upperPrice: number;
6
+ lowerPrice: number;
7
+ grids: number;
8
+ totalSize: number;
9
+ leverage?: number;
10
+ intervalSec?: number;
11
+ maxRuntime?: number;
12
+ trailingStop?: number;
13
+ }
14
+ export interface GridState {
15
+ gridLines: GridLine[];
16
+ activeOrders: Map<string, string>;
17
+ fills: number;
18
+ totalPnl: number;
19
+ peakEquity: number;
20
+ startedAt: number;
21
+ running: boolean;
22
+ }
23
+ interface GridLine {
24
+ price: number;
25
+ side: "buy" | "sell";
26
+ size: number;
27
+ filled: boolean;
28
+ }
29
+ export declare function runGrid(adapter: ExchangeAdapter, params: GridParams, jobId?: string, log?: (msg: string) => void): Promise<{
30
+ fills: number;
31
+ totalPnl: number;
32
+ runtime: number;
33
+ }>;
34
+ export {};
@@ -0,0 +1,185 @@
1
+ import { updateJobState } from "../jobs.js";
2
+ export async function runGrid(adapter, params, jobId, log = console.log) {
3
+ const { symbol, upperPrice, lowerPrice, grids, totalSize } = params;
4
+ const intervalMs = (params.intervalSec ?? 10) * 1000;
5
+ const maxRuntime = (params.maxRuntime ?? 0) * 1000;
6
+ const sizePerGrid = totalSize / grids;
7
+ if (upperPrice <= lowerPrice)
8
+ throw new Error("upperPrice must be > lowerPrice");
9
+ if (grids < 2)
10
+ throw new Error("Need at least 2 grid lines");
11
+ // Set leverage if specified
12
+ if (params.leverage) {
13
+ try {
14
+ await adapter.setLeverage(symbol, params.leverage);
15
+ log(`[GRID] Leverage set to ${params.leverage}x`);
16
+ }
17
+ catch {
18
+ log(`[GRID] Could not set leverage (may not be supported)`);
19
+ }
20
+ }
21
+ // Build grid lines
22
+ const step = (upperPrice - lowerPrice) / (grids - 1);
23
+ const gridLines = [];
24
+ for (let i = 0; i < grids; i++) {
25
+ gridLines.push({
26
+ price: lowerPrice + step * i,
27
+ side: "buy", // will be set based on current price
28
+ size: sizePerGrid,
29
+ filled: false,
30
+ });
31
+ }
32
+ log(`[GRID] ${symbol} ${params.side} | ${grids} grids | $${lowerPrice} - $${upperPrice} | step $${step.toFixed(2)}`);
33
+ log(`[GRID] Size per grid: ${sizePerGrid.toFixed(6)} | Total: ${totalSize}`);
34
+ const state = {
35
+ gridLines,
36
+ activeOrders: new Map(),
37
+ fills: 0,
38
+ totalPnl: 0,
39
+ peakEquity: 0,
40
+ startedAt: Date.now(),
41
+ running: true,
42
+ };
43
+ // Graceful shutdown
44
+ const shutdown = () => { state.running = false; };
45
+ process.on("SIGINT", shutdown);
46
+ process.on("SIGTERM", shutdown);
47
+ try {
48
+ // Get current price to determine buy/sell sides
49
+ const markets = await adapter.getMarkets();
50
+ const market = markets.find(m => m.symbol.toUpperCase() === symbol.toUpperCase());
51
+ const currentPrice = market ? parseFloat(market.markPrice) : (upperPrice + lowerPrice) / 2;
52
+ // Assign sides: buy below current, sell above current
53
+ for (const line of gridLines) {
54
+ if (params.side === "long") {
55
+ line.side = "buy";
56
+ }
57
+ else if (params.side === "short") {
58
+ line.side = "sell";
59
+ }
60
+ else {
61
+ line.side = line.price < currentPrice ? "buy" : "sell";
62
+ }
63
+ }
64
+ log(`[GRID] Current price: $${currentPrice.toFixed(2)} | Placing ${grids} limit orders...`);
65
+ // Place initial grid orders
66
+ await placeGridOrders(adapter, symbol, gridLines, state, log);
67
+ // Main loop: monitor fills and replace orders
68
+ while (state.running) {
69
+ await sleep(intervalMs);
70
+ if (maxRuntime > 0 && Date.now() - state.startedAt > maxRuntime) {
71
+ log(`[GRID] Max runtime reached. Stopping.`);
72
+ break;
73
+ }
74
+ try {
75
+ // Check open orders
76
+ const openOrders = await adapter.getOpenOrders();
77
+ const openIds = new Set(openOrders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase()).map(o => o.orderId));
78
+ // Find filled grid orders
79
+ let newFills = 0;
80
+ for (let i = 0; i < gridLines.length; i++) {
81
+ const orderId = state.activeOrders.get(String(i));
82
+ if (orderId && !openIds.has(orderId)) {
83
+ // Order filled — flip and replace
84
+ const line = gridLines[i];
85
+ state.fills++;
86
+ newFills++;
87
+ line.filled = true;
88
+ // Flip side: buy → sell, sell → buy (take profit at next grid)
89
+ const newSide = line.side === "buy" ? "sell" : "buy";
90
+ const newPrice = line.side === "buy"
91
+ ? line.price + step // sell one grid above
92
+ : line.price - step; // buy one grid below
93
+ if (newPrice >= lowerPrice && newPrice <= upperPrice) {
94
+ try {
95
+ const result = await adapter.limitOrder(symbol, newSide, String(newPrice.toFixed(2)), String(line.size));
96
+ const newOrderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
97
+ state.activeOrders.set(String(i), newOrderId);
98
+ line.side = newSide;
99
+ line.price = newPrice;
100
+ line.filled = false;
101
+ }
102
+ catch (err) {
103
+ const msg = err instanceof Error ? err.message : String(err);
104
+ log(`[GRID] Replace order error: ${msg}`);
105
+ }
106
+ }
107
+ else {
108
+ state.activeOrders.delete(String(i));
109
+ }
110
+ }
111
+ }
112
+ if (newFills > 0) {
113
+ // Estimate PnL from grid spacing
114
+ state.totalPnl += newFills * step * sizePerGrid;
115
+ log(`[GRID] ${newFills} fill(s) | Total fills: ${state.fills} | Est. PnL: $${state.totalPnl.toFixed(2)}`);
116
+ }
117
+ // Update job state
118
+ if (jobId) {
119
+ updateJobState(jobId, {
120
+ result: {
121
+ fills: state.fills,
122
+ totalPnl: state.totalPnl,
123
+ activeOrders: state.activeOrders.size,
124
+ runtime: Math.floor((Date.now() - state.startedAt) / 1000),
125
+ },
126
+ });
127
+ }
128
+ // Trailing stop check
129
+ if (params.trailingStop) {
130
+ const bal = await adapter.getBalance();
131
+ const equity = parseFloat(bal.equity);
132
+ if (equity > state.peakEquity)
133
+ state.peakEquity = equity;
134
+ const drawdown = ((state.peakEquity - equity) / state.peakEquity) * 100;
135
+ if (drawdown > params.trailingStop) {
136
+ log(`[GRID] Trailing stop triggered (${drawdown.toFixed(1)}% drawdown). Stopping.`);
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ catch (err) {
142
+ const msg = err instanceof Error ? err.message : String(err);
143
+ log(`[GRID] Monitor error: ${msg}`);
144
+ }
145
+ }
146
+ }
147
+ finally {
148
+ process.removeListener("SIGINT", shutdown);
149
+ process.removeListener("SIGTERM", shutdown);
150
+ // Cancel remaining grid orders
151
+ log(`[GRID] Cancelling remaining orders...`);
152
+ try {
153
+ await adapter.cancelAllOrders(symbol);
154
+ }
155
+ catch {
156
+ // best-effort
157
+ }
158
+ }
159
+ const runtime = Math.floor((Date.now() - state.startedAt) / 1000);
160
+ log(`[GRID] Done. ${state.fills} fills, est. PnL $${state.totalPnl.toFixed(2)}, runtime ${runtime}s`);
161
+ if (jobId) {
162
+ updateJobState(jobId, { status: "done", result: { fills: state.fills, totalPnl: state.totalPnl, runtime } });
163
+ }
164
+ return { fills: state.fills, totalPnl: state.totalPnl, runtime };
165
+ }
166
+ async function placeGridOrders(adapter, symbol, gridLines, state, log) {
167
+ let placed = 0;
168
+ for (let i = 0; i < gridLines.length; i++) {
169
+ const line = gridLines[i];
170
+ try {
171
+ const result = await adapter.limitOrder(symbol, line.side, String(line.price.toFixed(2)), String(line.size));
172
+ const orderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
173
+ state.activeOrders.set(String(i), orderId);
174
+ placed++;
175
+ }
176
+ catch (err) {
177
+ const msg = err instanceof Error ? err.message : String(err);
178
+ log(`[GRID] Order at $${line.price.toFixed(2)} failed: ${msg}`);
179
+ }
180
+ }
181
+ log(`[GRID] Placed ${placed}/${gridLines.length} orders`);
182
+ }
183
+ function sleep(ms) {
184
+ return new Promise((r) => setTimeout(r, ms));
185
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
2
+ export interface TrailingStopParams {
3
+ symbol: string;
4
+ trailPct: number;
5
+ intervalSec?: number;
6
+ activationPrice?: number;
7
+ }
8
+ export interface TrailingStopResult {
9
+ triggered: boolean;
10
+ reason: "triggered" | "cancelled" | "no_position";
11
+ peakPrice?: number;
12
+ triggerPrice?: number;
13
+ changePct?: number;
14
+ positionSide?: string;
15
+ runtime: number;
16
+ }
17
+ export declare function runTrailingStop(adapter: ExchangeAdapter, params: TrailingStopParams, jobId?: string, log?: (msg: string) => void): Promise<TrailingStopResult>;