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,48 @@
1
+ import type { ExchangeAdapter } from "./exchanges/interface.js";
2
+ export type PlanAction = "market_order" | "limit_order" | "stop_order" | "cancel_order" | "cancel_all" | "set_leverage" | "close_position" | "wait" | "check_balance" | "check_position";
3
+ export interface PlanStep {
4
+ id: string;
5
+ action: PlanAction;
6
+ params: Record<string, unknown>;
7
+ onFailure?: "abort" | "skip" | "rollback";
8
+ dependsOn?: string;
9
+ clientId?: string;
10
+ }
11
+ export interface ExecutionPlan {
12
+ version: "1.0";
13
+ exchange?: string;
14
+ description?: string;
15
+ steps: PlanStep[];
16
+ }
17
+ export interface StepResult {
18
+ stepId: string;
19
+ action: PlanAction;
20
+ status: "success" | "failed" | "skipped" | "rolled_back" | "dry_run";
21
+ result?: unknown;
22
+ error?: {
23
+ code: string;
24
+ message: string;
25
+ };
26
+ durationMs: number;
27
+ }
28
+ export interface PlanResult {
29
+ planId: string;
30
+ status: "completed" | "partial" | "failed" | "dry_run";
31
+ steps: StepResult[];
32
+ totalDurationMs: number;
33
+ timestamp: string;
34
+ }
35
+ /**
36
+ * Validate a plan structure without executing it.
37
+ */
38
+ export declare function validatePlan(plan: unknown): {
39
+ valid: boolean;
40
+ errors: string[];
41
+ };
42
+ /**
43
+ * Execute a plan step by step.
44
+ */
45
+ export declare function executePlan(adapter: ExchangeAdapter, plan: ExecutionPlan, opts?: {
46
+ dryRun?: boolean;
47
+ log?: (msg: string) => void;
48
+ }): Promise<PlanResult>;
@@ -0,0 +1,280 @@
1
+ import { symbolMatch } from "./utils.js";
2
+ import { logExecution } from "./execution-log.js";
3
+ import { randomUUID } from "crypto";
4
+ /**
5
+ * Validate a plan structure without executing it.
6
+ */
7
+ export function validatePlan(plan) {
8
+ const errors = [];
9
+ if (!plan || typeof plan !== "object") {
10
+ errors.push("Plan must be an object");
11
+ return { valid: false, errors };
12
+ }
13
+ const p = plan;
14
+ if (p.version !== "1.0")
15
+ errors.push(`Unsupported version: ${p.version} (expected "1.0")`);
16
+ if (!Array.isArray(p.steps)) {
17
+ errors.push("steps must be an array");
18
+ return { valid: false, errors };
19
+ }
20
+ if (p.steps.length === 0)
21
+ errors.push("Plan has no steps");
22
+ const validActions = new Set(["market_order", "limit_order", "stop_order", "cancel_order", "cancel_all", "set_leverage", "close_position", "wait", "check_balance", "check_position"]);
23
+ const ids = new Set();
24
+ for (let i = 0; i < p.steps.length; i++) {
25
+ const step = p.steps[i];
26
+ if (!step.id)
27
+ errors.push(`Step ${i}: missing id`);
28
+ if (ids.has(String(step.id)))
29
+ errors.push(`Step ${i}: duplicate id "${step.id}"`);
30
+ ids.add(String(step.id));
31
+ if (!step.action || !validActions.has(String(step.action)))
32
+ errors.push(`Step ${i}: invalid action "${step.action}"`);
33
+ if (step.onFailure && !["abort", "skip", "rollback"].includes(String(step.onFailure))) {
34
+ errors.push(`Step ${i}: invalid onFailure "${step.onFailure}"`);
35
+ }
36
+ if (step.dependsOn && !ids.has(String(step.dependsOn))) {
37
+ // dependsOn might reference a later step or non-existent
38
+ // Check if any step has that id
39
+ const found = p.steps.some(s => s.id === step.dependsOn);
40
+ if (!found)
41
+ errors.push(`Step ${i}: dependsOn "${step.dependsOn}" not found`);
42
+ }
43
+ // Validate params per action
44
+ const params = (step.params || {});
45
+ const action = String(step.action);
46
+ if (["market_order", "limit_order", "stop_order"].includes(action)) {
47
+ if (!params.symbol)
48
+ errors.push(`Step ${i} (${action}): missing params.symbol`);
49
+ if (!params.side)
50
+ errors.push(`Step ${i} (${action}): missing params.side`);
51
+ if (!params.size)
52
+ errors.push(`Step ${i} (${action}): missing params.size`);
53
+ }
54
+ if (action === "limit_order" && !params.price)
55
+ errors.push(`Step ${i} (limit_order): missing params.price`);
56
+ if (action === "stop_order" && !params.triggerPrice)
57
+ errors.push(`Step ${i} (stop_order): missing params.triggerPrice`);
58
+ if (action === "cancel_order" && (!params.symbol || !params.orderId))
59
+ errors.push(`Step ${i} (cancel_order): missing params.symbol or params.orderId`);
60
+ if (action === "set_leverage" && (!params.symbol || !params.leverage))
61
+ errors.push(`Step ${i} (set_leverage): missing params.symbol or params.leverage`);
62
+ if (action === "wait" && !params.ms)
63
+ errors.push(`Step ${i} (wait): missing params.ms`);
64
+ }
65
+ return { valid: errors.length === 0, errors };
66
+ }
67
+ /**
68
+ * Execute a plan step by step.
69
+ */
70
+ export async function executePlan(adapter, plan, opts) {
71
+ const planId = randomUUID().slice(0, 8);
72
+ const startTime = Date.now();
73
+ const results = [];
74
+ const completedSteps = new Map();
75
+ const dryRun = opts?.dryRun ?? false;
76
+ const log = opts?.log ?? (() => { });
77
+ log(`[plan:${planId}] Starting ${plan.steps.length} steps (${dryRun ? "DRY RUN" : "LIVE"})`);
78
+ for (const step of plan.steps) {
79
+ const stepStart = Date.now();
80
+ // Check dependency
81
+ if (step.dependsOn) {
82
+ const dep = completedSteps.get(step.dependsOn);
83
+ if (!dep || dep.status === "failed") {
84
+ const result = {
85
+ stepId: step.id,
86
+ action: step.action,
87
+ status: "skipped",
88
+ error: { code: "DEPENDENCY_FAILED", message: `Depends on ${step.dependsOn} which failed/missing` },
89
+ durationMs: 0,
90
+ };
91
+ results.push(result);
92
+ completedSteps.set(step.id, result);
93
+ continue;
94
+ }
95
+ }
96
+ if (dryRun) {
97
+ const result = {
98
+ stepId: step.id,
99
+ action: step.action,
100
+ status: "dry_run",
101
+ result: { params: step.params, wouldExecute: true },
102
+ durationMs: Date.now() - stepStart,
103
+ };
104
+ results.push(result);
105
+ completedSteps.set(step.id, result);
106
+ log(`[plan:${planId}] Step ${step.id} (${step.action}): dry run OK`);
107
+ continue;
108
+ }
109
+ try {
110
+ const result = await executeStep(adapter, step, log);
111
+ const stepResult = {
112
+ stepId: step.id,
113
+ action: step.action,
114
+ status: "success",
115
+ result,
116
+ durationMs: Date.now() - stepStart,
117
+ };
118
+ results.push(stepResult);
119
+ completedSteps.set(step.id, stepResult);
120
+ log(`[plan:${planId}] Step ${step.id} (${step.action}): success (${stepResult.durationMs}ms)`);
121
+ }
122
+ catch (err) {
123
+ const message = err instanceof Error ? err.message : String(err);
124
+ const onFailure = step.onFailure ?? "abort";
125
+ log(`[plan:${planId}] Step ${step.id} (${step.action}): FAILED — ${message}`);
126
+ if (onFailure === "skip") {
127
+ results.push({
128
+ stepId: step.id,
129
+ action: step.action,
130
+ status: "skipped",
131
+ error: { code: "STEP_FAILED", message },
132
+ durationMs: Date.now() - stepStart,
133
+ });
134
+ completedSteps.set(step.id, results[results.length - 1]);
135
+ continue;
136
+ }
137
+ if (onFailure === "rollback") {
138
+ log(`[plan:${planId}] Rolling back completed steps...`);
139
+ await rollbackSteps(adapter, results.filter(r => r.status === "success"), log);
140
+ results.push({
141
+ stepId: step.id,
142
+ action: step.action,
143
+ status: "rolled_back",
144
+ error: { code: "STEP_FAILED", message },
145
+ durationMs: Date.now() - stepStart,
146
+ });
147
+ return {
148
+ planId,
149
+ status: "failed",
150
+ steps: results,
151
+ totalDurationMs: Date.now() - startTime,
152
+ timestamp: new Date().toISOString(),
153
+ };
154
+ }
155
+ // abort
156
+ results.push({
157
+ stepId: step.id,
158
+ action: step.action,
159
+ status: "failed",
160
+ error: { code: "STEP_FAILED", message },
161
+ durationMs: Date.now() - stepStart,
162
+ });
163
+ return {
164
+ planId,
165
+ status: results.some(r => r.status === "success") ? "partial" : "failed",
166
+ steps: results,
167
+ totalDurationMs: Date.now() - startTime,
168
+ timestamp: new Date().toISOString(),
169
+ };
170
+ }
171
+ }
172
+ return {
173
+ planId,
174
+ status: dryRun ? "dry_run" : "completed",
175
+ steps: results,
176
+ totalDurationMs: Date.now() - startTime,
177
+ timestamp: new Date().toISOString(),
178
+ };
179
+ }
180
+ async function executeStep(adapter, step, log) {
181
+ const p = step.params;
182
+ const symbol = String(p.symbol ?? "").toUpperCase();
183
+ const side = String(p.side ?? "");
184
+ const size = String(p.size ?? "");
185
+ switch (step.action) {
186
+ case "market_order": {
187
+ const result = await adapter.marketOrder(symbol, side, size);
188
+ logExecution({ type: "market_order", exchange: adapter.name, symbol, side, size, status: "success", dryRun: false, meta: { planStep: step.id, clientId: step.clientId } });
189
+ return result;
190
+ }
191
+ case "limit_order": {
192
+ const price = String(p.price ?? "");
193
+ const result = await adapter.limitOrder(symbol, side, price, size);
194
+ logExecution({ type: "limit_order", exchange: adapter.name, symbol, side, size, price, status: "success", dryRun: false, meta: { planStep: step.id, clientId: step.clientId } });
195
+ return result;
196
+ }
197
+ case "stop_order": {
198
+ const triggerPrice = String(p.triggerPrice ?? "");
199
+ const limitPrice = p.limitPrice ? String(p.limitPrice) : undefined;
200
+ return adapter.stopOrder(symbol, side, size, triggerPrice, { limitPrice, reduceOnly: !!p.reduceOnly });
201
+ }
202
+ case "cancel_order": {
203
+ return adapter.cancelOrder(symbol, String(p.orderId ?? ""));
204
+ }
205
+ case "cancel_all": {
206
+ return adapter.cancelAllOrders(symbol || undefined);
207
+ }
208
+ case "set_leverage": {
209
+ const lev = Number(p.leverage);
210
+ const mode = p.marginMode ?? "cross";
211
+ return adapter.setLeverage(symbol, lev, mode);
212
+ }
213
+ case "close_position": {
214
+ const positions = await adapter.getPositions();
215
+ const pos = positions.find(pp => symbolMatch(pp.symbol, symbol));
216
+ if (!pos)
217
+ throw new Error(`No position found for ${symbol}`);
218
+ const closeSide = pos.side === "long" ? "sell" : "buy";
219
+ const result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
220
+ logExecution({ type: "market_order", exchange: adapter.name, symbol, side: closeSide, size: pos.size, status: "success", dryRun: false, meta: { planStep: step.id, action: "close_position" } });
221
+ return result;
222
+ }
223
+ case "wait": {
224
+ const ms = Number(p.ms ?? 1000);
225
+ log(`[wait] ${ms}ms`);
226
+ await new Promise(r => setTimeout(r, ms));
227
+ return { waited: ms };
228
+ }
229
+ case "check_balance": {
230
+ const balance = await adapter.getBalance();
231
+ const minAvailable = Number(p.minAvailable ?? 0);
232
+ if (minAvailable > 0 && Number(balance.available) < minAvailable) {
233
+ throw new Error(`Balance check failed: $${balance.available} available < $${minAvailable} required`);
234
+ }
235
+ return balance;
236
+ }
237
+ case "check_position": {
238
+ const positions = await adapter.getPositions();
239
+ const pos = positions.find(pp => symbolMatch(pp.symbol, symbol));
240
+ if (p.mustExist && !pos)
241
+ throw new Error(`Position ${symbol} not found`);
242
+ if (p.mustNotExist && pos)
243
+ throw new Error(`Position ${symbol} exists but should not`);
244
+ return pos ?? { symbol, exists: false };
245
+ }
246
+ default:
247
+ throw new Error(`Unknown action: ${step.action}`);
248
+ }
249
+ }
250
+ /**
251
+ * Attempt to rollback completed steps by performing inverse operations.
252
+ */
253
+ async function rollbackSteps(adapter, completedSteps, log) {
254
+ // Rollback in reverse order
255
+ for (let i = completedSteps.length - 1; i >= 0; i--) {
256
+ const step = completedSteps[i];
257
+ try {
258
+ switch (step.action) {
259
+ case "market_order":
260
+ case "limit_order": {
261
+ // Close the position that was opened
262
+ // This is best-effort — market conditions may have changed
263
+ log(`[rollback] Attempting to reverse ${step.action} ${step.stepId}`);
264
+ // We'd need position info; just cancel any pending orders as safety measure
265
+ await adapter.cancelAllOrders().catch(() => { });
266
+ break;
267
+ }
268
+ case "cancel_order":
269
+ case "cancel_all":
270
+ // Can't un-cancel, skip
271
+ break;
272
+ default:
273
+ break;
274
+ }
275
+ }
276
+ catch (err) {
277
+ log(`[rollback] Failed to rollback step ${step.stepId}: ${err instanceof Error ? err.message : err}`);
278
+ }
279
+ }
280
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Position history — persists position lifecycle events to ~/.perp/positions.jsonl
3
+ * Enables agents to review past trades, analyze P&L, and learn from history.
4
+ */
5
+ import type { StreamEvent } from "./event-stream.js";
6
+ export interface PositionRecord {
7
+ id: string;
8
+ exchange: string;
9
+ symbol: string;
10
+ side: "long" | "short";
11
+ entryPrice: string;
12
+ exitPrice?: string;
13
+ size: string;
14
+ realizedPnl?: string;
15
+ unrealizedPnl?: string;
16
+ openedAt: string;
17
+ closedAt?: string;
18
+ updatedAt: string;
19
+ status: "open" | "closed" | "updated";
20
+ duration?: number;
21
+ meta?: Record<string, unknown>;
22
+ }
23
+ export interface PositionStats {
24
+ totalTrades: number;
25
+ wins: number;
26
+ losses: number;
27
+ winRate: number;
28
+ totalPnl: number;
29
+ avgPnl: number;
30
+ bestTrade: number;
31
+ worstTrade: number;
32
+ avgDuration: number;
33
+ longestTrade: number;
34
+ shortestTrade: number;
35
+ bySymbol: Record<string, {
36
+ trades: number;
37
+ pnl: number;
38
+ winRate: number;
39
+ }>;
40
+ byExchange: Record<string, {
41
+ trades: number;
42
+ pnl: number;
43
+ }>;
44
+ }
45
+ /** Append a position record to the log */
46
+ export declare function logPosition(record: PositionRecord): void;
47
+ /** Read position history with optional filters */
48
+ export declare function readPositionHistory(opts?: {
49
+ symbol?: string;
50
+ exchange?: string;
51
+ status?: string;
52
+ limit?: number;
53
+ since?: string;
54
+ }): PositionRecord[];
55
+ /** Compute aggregate stats from closed position records */
56
+ export declare function getPositionStats(opts?: {
57
+ exchange?: string;
58
+ since?: string;
59
+ }): PositionStats;
60
+ /**
61
+ * Returns a wrapper around the event callback that intercepts position events
62
+ * and logs them to the position history file.
63
+ *
64
+ * Usage:
65
+ * const loggedOnEvent = attachPositionLogger(originalOnEvent);
66
+ * startEventStream(adapter, { onEvent: loggedOnEvent, ... });
67
+ */
68
+ export declare function attachPositionLogger(onEvent: (event: StreamEvent) => void): (event: StreamEvent) => void;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Position history — persists position lifecycle events to ~/.perp/positions.jsonl
3
+ * Enables agents to review past trades, analyze P&L, and learn from history.
4
+ */
5
+ import { existsSync, appendFileSync, readFileSync, mkdirSync } from "fs";
6
+ import { resolve } from "path";
7
+ const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
8
+ const POSITIONS_FILE = resolve(PERP_DIR, "positions.jsonl");
9
+ function ensureDir() {
10
+ if (!existsSync(PERP_DIR))
11
+ mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
12
+ }
13
+ function genId() {
14
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
15
+ }
16
+ /** Append a position record to the log */
17
+ export function logPosition(record) {
18
+ ensureDir();
19
+ appendFileSync(POSITIONS_FILE, JSON.stringify(record) + "\n", { mode: 0o600 });
20
+ }
21
+ /** Read position history with optional filters */
22
+ export function readPositionHistory(opts) {
23
+ if (!existsSync(POSITIONS_FILE))
24
+ return [];
25
+ const lines = readFileSync(POSITIONS_FILE, "utf-8").trim().split("\n").filter(Boolean);
26
+ let records = lines.map(line => {
27
+ try {
28
+ return JSON.parse(line);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }).filter(Boolean);
34
+ // Apply filters
35
+ if (opts?.exchange) {
36
+ records = records.filter(r => r.exchange === opts.exchange);
37
+ }
38
+ if (opts?.symbol) {
39
+ records = records.filter(r => r.symbol.toUpperCase().includes(opts.symbol.toUpperCase()));
40
+ }
41
+ if (opts?.status) {
42
+ records = records.filter(r => r.status === opts.status);
43
+ }
44
+ if (opts?.since) {
45
+ const sinceDate = new Date(opts.since).getTime();
46
+ records = records.filter(r => new Date(r.updatedAt).getTime() >= sinceDate);
47
+ }
48
+ // Sort newest first
49
+ records.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
50
+ // Limit
51
+ if (opts?.limit) {
52
+ records = records.slice(0, opts.limit);
53
+ }
54
+ return records;
55
+ }
56
+ /** Compute aggregate stats from closed position records */
57
+ export function getPositionStats(opts) {
58
+ const records = readPositionHistory({
59
+ exchange: opts?.exchange,
60
+ status: "closed",
61
+ since: opts?.since,
62
+ });
63
+ const stats = {
64
+ totalTrades: 0,
65
+ wins: 0,
66
+ losses: 0,
67
+ winRate: 0,
68
+ totalPnl: 0,
69
+ avgPnl: 0,
70
+ bestTrade: 0,
71
+ worstTrade: 0,
72
+ avgDuration: 0,
73
+ longestTrade: 0,
74
+ shortestTrade: Infinity,
75
+ bySymbol: {},
76
+ byExchange: {},
77
+ };
78
+ if (records.length === 0) {
79
+ stats.shortestTrade = 0;
80
+ return stats;
81
+ }
82
+ let totalDuration = 0;
83
+ let durationCount = 0;
84
+ for (const r of records) {
85
+ stats.totalTrades++;
86
+ const pnl = Number(r.realizedPnl ?? 0);
87
+ if (pnl > 0)
88
+ stats.wins++;
89
+ else if (pnl < 0)
90
+ stats.losses++;
91
+ stats.totalPnl += pnl;
92
+ if (pnl > stats.bestTrade)
93
+ stats.bestTrade = pnl;
94
+ if (pnl < stats.worstTrade)
95
+ stats.worstTrade = pnl;
96
+ if (r.duration !== undefined && r.duration > 0) {
97
+ totalDuration += r.duration;
98
+ durationCount++;
99
+ if (r.duration > stats.longestTrade)
100
+ stats.longestTrade = r.duration;
101
+ if (r.duration < stats.shortestTrade)
102
+ stats.shortestTrade = r.duration;
103
+ }
104
+ // By symbol
105
+ if (!stats.bySymbol[r.symbol]) {
106
+ stats.bySymbol[r.symbol] = { trades: 0, pnl: 0, winRate: 0 };
107
+ }
108
+ stats.bySymbol[r.symbol].trades++;
109
+ stats.bySymbol[r.symbol].pnl += pnl;
110
+ // By exchange
111
+ if (!stats.byExchange[r.exchange]) {
112
+ stats.byExchange[r.exchange] = { trades: 0, pnl: 0 };
113
+ }
114
+ stats.byExchange[r.exchange].trades++;
115
+ stats.byExchange[r.exchange].pnl += pnl;
116
+ }
117
+ stats.winRate = stats.totalTrades > 0 ? (stats.wins / stats.totalTrades) * 100 : 0;
118
+ stats.avgPnl = stats.totalTrades > 0 ? stats.totalPnl / stats.totalTrades : 0;
119
+ stats.avgDuration = durationCount > 0 ? totalDuration / durationCount : 0;
120
+ if (stats.shortestTrade === Infinity)
121
+ stats.shortestTrade = 0;
122
+ // Compute per-symbol win rates
123
+ // We need to recount wins per symbol from the records
124
+ const symbolWins = {};
125
+ for (const r of records) {
126
+ const pnl = Number(r.realizedPnl ?? 0);
127
+ if (pnl > 0) {
128
+ symbolWins[r.symbol] = (symbolWins[r.symbol] ?? 0) + 1;
129
+ }
130
+ }
131
+ for (const sym of Object.keys(stats.bySymbol)) {
132
+ const wins = symbolWins[sym] ?? 0;
133
+ stats.bySymbol[sym].winRate = stats.bySymbol[sym].trades > 0
134
+ ? (wins / stats.bySymbol[sym].trades) * 100
135
+ : 0;
136
+ }
137
+ return stats;
138
+ }
139
+ /**
140
+ * Returns a wrapper around the event callback that intercepts position events
141
+ * and logs them to the position history file.
142
+ *
143
+ * Usage:
144
+ * const loggedOnEvent = attachPositionLogger(originalOnEvent);
145
+ * startEventStream(adapter, { onEvent: loggedOnEvent, ... });
146
+ */
147
+ export function attachPositionLogger(onEvent) {
148
+ // Track open positions to compute duration on close
149
+ const openPositions = new Map();
150
+ return (event) => {
151
+ const ts = event.timestamp;
152
+ const exchange = event.exchange;
153
+ const data = event.data;
154
+ if (event.type === "position_opened") {
155
+ const id = genId();
156
+ const symbol = String(data.symbol ?? "");
157
+ const side = String(data.side ?? "long");
158
+ const key = `${exchange}:${symbol}`;
159
+ openPositions.set(key, { id, openedAt: ts, entryPrice: String(data.entryPrice ?? "") });
160
+ logPosition({
161
+ id,
162
+ exchange,
163
+ symbol,
164
+ side,
165
+ entryPrice: String(data.entryPrice ?? ""),
166
+ size: String(data.size ?? ""),
167
+ unrealizedPnl: String(data.unrealizedPnl ?? ""),
168
+ openedAt: ts,
169
+ updatedAt: ts,
170
+ status: "open",
171
+ });
172
+ }
173
+ else if (event.type === "position_updated") {
174
+ const symbol = String(data.symbol ?? "");
175
+ const side = String(data.side ?? "long");
176
+ const key = `${exchange}:${symbol}`;
177
+ const tracked = openPositions.get(key);
178
+ logPosition({
179
+ id: tracked?.id ?? genId(),
180
+ exchange,
181
+ symbol,
182
+ side,
183
+ entryPrice: String(data.entryPrice ?? tracked?.entryPrice ?? ""),
184
+ size: String(data.size ?? ""),
185
+ unrealizedPnl: String(data.unrealizedPnl ?? ""),
186
+ openedAt: tracked?.openedAt ?? ts,
187
+ updatedAt: ts,
188
+ status: "updated",
189
+ meta: {
190
+ prevSize: data.prevSize,
191
+ prevSide: data.prevSide,
192
+ },
193
+ });
194
+ }
195
+ else if (event.type === "position_closed") {
196
+ const symbol = String(data.symbol ?? "");
197
+ const side = String(data.side ?? "long");
198
+ const key = `${exchange}:${symbol}`;
199
+ const tracked = openPositions.get(key);
200
+ const openedAt = tracked?.openedAt ?? ts;
201
+ const duration = new Date(ts).getTime() - new Date(openedAt).getTime();
202
+ const realizedPnl = String(data.unrealizedPnl ?? "0");
203
+ logPosition({
204
+ id: tracked?.id ?? genId(),
205
+ exchange,
206
+ symbol,
207
+ side,
208
+ entryPrice: String(data.entryPrice ?? tracked?.entryPrice ?? ""),
209
+ size: String(data.size ?? ""),
210
+ realizedPnl,
211
+ openedAt,
212
+ closedAt: ts,
213
+ updatedAt: ts,
214
+ status: "closed",
215
+ duration: duration > 0 ? duration : undefined,
216
+ });
217
+ openPositions.delete(key);
218
+ }
219
+ // Always forward the event to the original callback
220
+ onEvent(event);
221
+ };
222
+ }
@@ -0,0 +1,64 @@
1
+ import type { ExchangeAdapter } from "./exchanges/interface.js";
2
+ /**
3
+ * Cross-exchange rebalancing engine.
4
+ *
5
+ * Checks balances across exchanges, computes a rebalancing plan,
6
+ * and optionally executes withdraw → bridge → deposit pipeline.
7
+ */
8
+ export interface ExchangeBalanceSnapshot {
9
+ exchange: string;
10
+ equity: number;
11
+ available: number;
12
+ marginUsed: number;
13
+ unrealizedPnl: number;
14
+ }
15
+ export interface RebalancePlan {
16
+ snapshots: ExchangeBalanceSnapshot[];
17
+ totalEquity: number;
18
+ targetPerExchange: number;
19
+ moves: RebalanceMove[];
20
+ summary: string;
21
+ }
22
+ export interface RebalanceMove {
23
+ from: string;
24
+ to: string;
25
+ amount: number;
26
+ reason: string;
27
+ }
28
+ /**
29
+ * Fetch balances from all exchanges in parallel.
30
+ */
31
+ export declare function fetchAllBalances(adapters: Map<string, ExchangeAdapter>): Promise<ExchangeBalanceSnapshot[]>;
32
+ /**
33
+ * Compute a rebalancing plan to equalize available balance across exchanges.
34
+ *
35
+ * Strategy: equal-weight — target = totalAvailable / numExchanges
36
+ * Only moves from exchanges with surplus to those with deficit.
37
+ * Respects a minimum move threshold to avoid tiny transfers.
38
+ */
39
+ export declare function computeRebalancePlan(snapshots: ExchangeBalanceSnapshot[], opts?: {
40
+ /** Minimum move amount to bother with ($) */
41
+ minMove?: number;
42
+ /** Target allocation weights (default: equal). Keys = exchange names, values = 0-1 weights summing to 1 */
43
+ weights?: Record<string, number>;
44
+ /** Reserve: keep at least this much available on each exchange */
45
+ reserve?: number;
46
+ }): RebalancePlan;
47
+ /**
48
+ * Check if an exchange has enough available balance for a given trade size.
49
+ */
50
+ export declare function hasEnoughBalance(snapshots: ExchangeBalanceSnapshot[], exchange: string, requiredUsd: number, marginBuffer?: number): boolean;
51
+ /**
52
+ * Get the chain/bridge info for an exchange.
53
+ */
54
+ export declare function getExchangeChain(exchange: string): {
55
+ chain: string;
56
+ };
57
+ /**
58
+ * Describe the bridge route for a rebalance move.
59
+ */
60
+ export declare function describeBridgeRoute(move: RebalanceMove): string;
61
+ /**
62
+ * Estimate total time for a rebalance move.
63
+ */
64
+ export declare function estimateMoveTime(move: RebalanceMove): string;