perp-cli 0.9.8 → 0.12.0

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 (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -1,314 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { validatePlan, executePlan } from "../plan-executor.js";
3
- // Mock execution-log to prevent file I/O during tests
4
- vi.mock("../execution-log.js", () => ({
5
- logExecution: vi.fn().mockReturnValue({ id: "mock", timestamp: new Date().toISOString() }),
6
- }));
7
- // ── Mock adapter ──
8
- function mockAdapter(overrides) {
9
- return {
10
- name: "test",
11
- getMarkets: vi.fn().mockResolvedValue([]),
12
- getBalance: vi.fn().mockResolvedValue({ equity: "1000", available: "800", marginUsed: "200", unrealizedPnl: "0" }),
13
- getPositions: vi.fn().mockResolvedValue([]),
14
- getOpenOrders: vi.fn().mockResolvedValue([]),
15
- getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
16
- marketOrder: vi.fn().mockResolvedValue({ orderId: "m1", status: "filled" }),
17
- limitOrder: vi.fn().mockResolvedValue({ orderId: "l1", status: "open" }),
18
- stopOrder: vi.fn().mockResolvedValue({ orderId: "s1" }),
19
- cancelOrder: vi.fn().mockResolvedValue({ success: true }),
20
- cancelAllOrders: vi.fn().mockResolvedValue({ cancelled: 0 }),
21
- setLeverage: vi.fn().mockResolvedValue({ leverage: 10 }),
22
- ...overrides,
23
- };
24
- }
25
- describe("validatePlan — structural validation", () => {
26
- it("accepts valid plan", () => {
27
- const plan = {
28
- version: "1.0",
29
- steps: [
30
- { id: "1", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.1" } },
31
- { id: "2", action: "wait", params: { ms: 500 } },
32
- ],
33
- };
34
- const r = validatePlan(plan);
35
- expect(r.valid).toBe(true);
36
- expect(r.errors).toHaveLength(0);
37
- });
38
- it("rejects non-object input", () => {
39
- expect(validatePlan(null).valid).toBe(false);
40
- expect(validatePlan("bad").valid).toBe(false);
41
- });
42
- it("rejects wrong version", () => {
43
- const r = validatePlan({ version: "2.0", steps: [{ id: "1", action: "wait", params: { ms: 1 } }] });
44
- expect(r.valid).toBe(false);
45
- expect(r.errors[0]).toContain("version");
46
- });
47
- it("rejects missing steps", () => {
48
- const r = validatePlan({ version: "1.0" });
49
- expect(r.valid).toBe(false);
50
- });
51
- it("rejects empty steps array", () => {
52
- const r = validatePlan({ version: "1.0", steps: [] });
53
- expect(r.valid).toBe(false);
54
- expect(r.errors[0]).toContain("no steps");
55
- });
56
- it("rejects duplicate step IDs", () => {
57
- const r = validatePlan({
58
- version: "1.0",
59
- steps: [
60
- { id: "dup", action: "wait", params: { ms: 1 } },
61
- { id: "dup", action: "wait", params: { ms: 2 } },
62
- ],
63
- });
64
- expect(r.valid).toBe(false);
65
- expect(r.errors.some(e => e.includes("duplicate"))).toBe(true);
66
- });
67
- it("rejects invalid action", () => {
68
- const r = validatePlan({ version: "1.0", steps: [{ id: "1", action: "fly_to_moon", params: {} }] });
69
- expect(r.valid).toBe(false);
70
- expect(r.errors[0]).toContain("invalid action");
71
- });
72
- it("requires symbol/side/size for order actions", () => {
73
- const r = validatePlan({
74
- version: "1.0",
75
- steps: [{ id: "1", action: "market_order", params: {} }],
76
- });
77
- expect(r.valid).toBe(false);
78
- expect(r.errors.some(e => e.includes("symbol"))).toBe(true);
79
- expect(r.errors.some(e => e.includes("side"))).toBe(true);
80
- expect(r.errors.some(e => e.includes("size"))).toBe(true);
81
- });
82
- it("requires price for limit_order", () => {
83
- const r = validatePlan({
84
- version: "1.0",
85
- steps: [{ id: "1", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "1" } }],
86
- });
87
- expect(r.valid).toBe(false);
88
- expect(r.errors.some(e => e.includes("price"))).toBe(true);
89
- });
90
- it("requires triggerPrice for stop_order", () => {
91
- const r = validatePlan({
92
- version: "1.0",
93
- steps: [{ id: "1", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "1" } }],
94
- });
95
- expect(r.valid).toBe(false);
96
- expect(r.errors.some(e => e.includes("triggerPrice"))).toBe(true);
97
- });
98
- it("requires ms for wait", () => {
99
- const r = validatePlan({ version: "1.0", steps: [{ id: "1", action: "wait", params: {} }] });
100
- expect(r.valid).toBe(false);
101
- expect(r.errors.some(e => e.includes("ms"))).toBe(true);
102
- });
103
- it("rejects invalid onFailure value", () => {
104
- const r = validatePlan({
105
- version: "1.0",
106
- steps: [{ id: "1", action: "wait", params: { ms: 1 }, onFailure: "panic" }],
107
- });
108
- expect(r.valid).toBe(false);
109
- expect(r.errors.some(e => e.includes("onFailure"))).toBe(true);
110
- });
111
- it("detects broken dependsOn reference", () => {
112
- const r = validatePlan({
113
- version: "1.0",
114
- steps: [{ id: "1", action: "wait", params: { ms: 1 }, dependsOn: "nonexistent" }],
115
- });
116
- expect(r.valid).toBe(false);
117
- expect(r.errors.some(e => e.includes("dependsOn"))).toBe(true);
118
- });
119
- });
120
- describe("executePlan — dry run", () => {
121
- it("dry run produces dry_run status for all steps", async () => {
122
- const plan = {
123
- version: "1.0",
124
- steps: [
125
- { id: "1", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.1" } },
126
- { id: "2", action: "check_balance", params: {} },
127
- ],
128
- };
129
- const adapter = mockAdapter();
130
- const r = await executePlan(adapter, plan, { dryRun: true });
131
- expect(r.status).toBe("dry_run");
132
- expect(r.steps).toHaveLength(2);
133
- for (const s of r.steps) {
134
- expect(s.status).toBe("dry_run");
135
- }
136
- // No actual calls should have been made
137
- expect(adapter.marketOrder).not.toHaveBeenCalled();
138
- });
139
- });
140
- describe("executePlan — live execution", () => {
141
- it("executes market_order and wait sequentially", async () => {
142
- const plan = {
143
- version: "1.0",
144
- steps: [
145
- { id: "1", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.1" } },
146
- { id: "2", action: "wait", params: { ms: 10 } },
147
- ],
148
- };
149
- const adapter = mockAdapter();
150
- const r = await executePlan(adapter, plan);
151
- expect(r.status).toBe("completed");
152
- expect(r.steps).toHaveLength(2);
153
- expect(r.steps[0].status).toBe("success");
154
- expect(r.steps[1].status).toBe("success");
155
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0.1");
156
- });
157
- it("executes check_balance and validates minimum", async () => {
158
- const adapter = mockAdapter();
159
- const plan = {
160
- version: "1.0",
161
- steps: [{ id: "1", action: "check_balance", params: { minAvailable: 500 } }],
162
- };
163
- const r = await executePlan(adapter, plan);
164
- expect(r.status).toBe("completed");
165
- expect(r.steps[0].status).toBe("success");
166
- });
167
- it("fails check_balance when balance is too low", async () => {
168
- const adapter = mockAdapter({
169
- getBalance: vi.fn().mockResolvedValue({ equity: "100", available: "50", marginUsed: "50", unrealizedPnl: "0" }),
170
- });
171
- const plan = {
172
- version: "1.0",
173
- steps: [{ id: "1", action: "check_balance", params: { minAvailable: 500 } }],
174
- };
175
- const r = await executePlan(adapter, plan);
176
- expect(r.status).toBe("failed");
177
- expect(r.steps[0].status).toBe("failed");
178
- });
179
- });
180
- describe("executePlan — failure modes", () => {
181
- it("aborts on failure by default", async () => {
182
- const adapter = mockAdapter({
183
- marketOrder: vi.fn().mockRejectedValue(new Error("Insufficient balance")),
184
- });
185
- const plan = {
186
- version: "1.0",
187
- steps: [
188
- { id: "1", action: "market_order", params: { symbol: "BTC", side: "buy", size: "100" } },
189
- { id: "2", action: "wait", params: { ms: 1 } },
190
- ],
191
- };
192
- const r = await executePlan(adapter, plan);
193
- expect(r.status).toBe("failed");
194
- expect(r.steps).toHaveLength(1); // step 2 never reached
195
- expect(r.steps[0].status).toBe("failed");
196
- });
197
- it("skips failed step when onFailure=skip", async () => {
198
- const adapter = mockAdapter({
199
- marketOrder: vi.fn().mockRejectedValue(new Error("Insufficient balance")),
200
- });
201
- const plan = {
202
- version: "1.0",
203
- steps: [
204
- { id: "1", action: "market_order", params: { symbol: "BTC", side: "buy", size: "100" }, onFailure: "skip" },
205
- { id: "2", action: "wait", params: { ms: 1 } },
206
- ],
207
- };
208
- const r = await executePlan(adapter, plan);
209
- expect(r.status).toBe("completed");
210
- expect(r.steps[0].status).toBe("skipped");
211
- expect(r.steps[1].status).toBe("success");
212
- });
213
- it("rolls back on failure when onFailure=rollback", async () => {
214
- const adapter = mockAdapter({
215
- limitOrder: vi.fn().mockRejectedValue(new Error("Price stale")),
216
- });
217
- const plan = {
218
- version: "1.0",
219
- steps: [
220
- { id: "1", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.1" } },
221
- { id: "2", action: "limit_order", params: { symbol: "ETH", side: "sell", size: "1", price: "2000" }, onFailure: "rollback" },
222
- ],
223
- };
224
- const r = await executePlan(adapter, plan);
225
- expect(r.status).toBe("failed");
226
- expect(r.steps[0].status).toBe("success");
227
- expect(r.steps[1].status).toBe("rolled_back");
228
- // cancelAllOrders called during rollback
229
- expect(adapter.cancelAllOrders).toHaveBeenCalled();
230
- });
231
- it("skips step when dependency failed (abort)", async () => {
232
- const adapter = mockAdapter({
233
- marketOrder: vi.fn().mockRejectedValue(new Error("fail")),
234
- // Step 1 will abort by default, so step 2 never runs
235
- });
236
- const plan = {
237
- version: "1.0",
238
- steps: [
239
- { id: "1", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.1" } },
240
- { id: "2", action: "wait", params: { ms: 1 }, dependsOn: "1" },
241
- ],
242
- };
243
- const r = await executePlan(adapter, plan);
244
- expect(r.status).toBe("failed");
245
- expect(r.steps).toHaveLength(1); // step 2 never reached due to abort
246
- expect(r.steps[0].status).toBe("failed");
247
- });
248
- it("skips dependent step when dependency status is failed", async () => {
249
- // Use two steps where step 1 fails with skip, then step 2 depends on step 3 which fails
250
- const adapter = mockAdapter({
251
- limitOrder: vi.fn().mockRejectedValue(new Error("fail")),
252
- });
253
- const plan = {
254
- version: "1.0",
255
- steps: [
256
- { id: "1", action: "wait", params: { ms: 1 } },
257
- { id: "2", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "1", price: "50000" }, onFailure: "skip" },
258
- { id: "3", action: "wait", params: { ms: 1 }, dependsOn: "2" },
259
- ],
260
- };
261
- const r = await executePlan(adapter, plan);
262
- // Step 2 is skipped (onFailure=skip), step 3 runs because "skipped" != "failed"
263
- expect(r.steps[0].status).toBe("success");
264
- expect(r.steps[1].status).toBe("skipped");
265
- // Step 3 still runs — skipped deps are treated as present but not "failed"
266
- expect(r.steps[2].status).toBe("success");
267
- });
268
- });
269
- describe("executePlan — action types", () => {
270
- it("executes set_leverage", async () => {
271
- const adapter = mockAdapter();
272
- const plan = {
273
- version: "1.0",
274
- steps: [{ id: "1", action: "set_leverage", params: { symbol: "BTC", leverage: 10, marginMode: "cross" } }],
275
- };
276
- const r = await executePlan(adapter, plan);
277
- expect(r.status).toBe("completed");
278
- expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 10, "cross");
279
- });
280
- it("executes cancel_order", async () => {
281
- const adapter = mockAdapter();
282
- const plan = {
283
- version: "1.0",
284
- steps: [{ id: "1", action: "cancel_order", params: { symbol: "BTC", orderId: "abc123" } }],
285
- };
286
- const r = await executePlan(adapter, plan);
287
- expect(r.status).toBe("completed");
288
- expect(adapter.cancelOrder).toHaveBeenCalledWith("BTC", "abc123");
289
- });
290
- it("executes close_position", async () => {
291
- const adapter = mockAdapter({
292
- getPositions: vi.fn().mockResolvedValue([
293
- { symbol: "BTC", side: "long", size: "0.5", entryPrice: "65000", unrealizedPnl: "100", liquidationPrice: "60000", markPrice: "66000", leverage: "10", marginMode: "cross" },
294
- ]),
295
- });
296
- const plan = {
297
- version: "1.0",
298
- steps: [{ id: "1", action: "close_position", params: { symbol: "BTC" } }],
299
- };
300
- const r = await executePlan(adapter, plan);
301
- expect(r.status).toBe("completed");
302
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.5");
303
- });
304
- it("fails close_position when no position exists", async () => {
305
- const adapter = mockAdapter();
306
- const plan = {
307
- version: "1.0",
308
- steps: [{ id: "1", action: "close_position", params: { symbol: "BTC" } }],
309
- };
310
- const r = await executePlan(adapter, plan);
311
- expect(r.status).toBe("failed");
312
- expect(r.steps[0].error?.message).toContain("No position found");
313
- });
314
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,367 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { existsSync, unlinkSync, readFileSync, writeFileSync, renameSync } from "fs";
3
- import { resolve } from "path";
4
- import { logPosition, readPositionHistory, getPositionStats, attachPositionLogger, } from "../position-history.js";
5
- const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
6
- const POSITIONS_FILE = resolve(PERP_DIR, "positions.jsonl");
7
- const BACKUP_FILE = resolve(PERP_DIR, "positions.jsonl.test-backup");
8
- beforeEach(() => {
9
- // Backup existing file if present
10
- if (existsSync(POSITIONS_FILE)) {
11
- const content = readFileSync(POSITIONS_FILE, "utf-8");
12
- writeFileSync(BACKUP_FILE, content);
13
- }
14
- // Clear for test
15
- if (existsSync(POSITIONS_FILE))
16
- unlinkSync(POSITIONS_FILE);
17
- });
18
- afterEach(() => {
19
- // Restore original file
20
- if (existsSync(POSITIONS_FILE))
21
- unlinkSync(POSITIONS_FILE);
22
- if (existsSync(BACKUP_FILE)) {
23
- renameSync(BACKUP_FILE, POSITIONS_FILE);
24
- }
25
- });
26
- function makeRecord(overrides) {
27
- return {
28
- id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
29
- exchange: "test",
30
- symbol: "BTC",
31
- side: "long",
32
- entryPrice: "100000",
33
- size: "0.1",
34
- openedAt: new Date().toISOString(),
35
- updatedAt: new Date().toISOString(),
36
- status: "open",
37
- ...overrides,
38
- };
39
- }
40
- describe("logPosition + readPositionHistory", () => {
41
- it("should log a position record and read it back", () => {
42
- const record = makeRecord({ symbol: "BTC", side: "long" });
43
- logPosition(record);
44
- const records = readPositionHistory();
45
- expect(records).toHaveLength(1);
46
- expect(records[0].symbol).toBe("BTC");
47
- expect(records[0].side).toBe("long");
48
- expect(records[0].id).toBe(record.id);
49
- });
50
- it("should log multiple records", () => {
51
- logPosition(makeRecord({ symbol: "BTC" }));
52
- logPosition(makeRecord({ symbol: "ETH" }));
53
- logPosition(makeRecord({ symbol: "SOL" }));
54
- const records = readPositionHistory();
55
- expect(records).toHaveLength(3);
56
- });
57
- it("should sort newest first", async () => {
58
- const now = Date.now();
59
- logPosition(makeRecord({ symbol: "FIRST", updatedAt: new Date(now - 2000).toISOString() }));
60
- logPosition(makeRecord({ symbol: "SECOND", updatedAt: new Date(now - 1000).toISOString() }));
61
- logPosition(makeRecord({ symbol: "THIRD", updatedAt: new Date(now).toISOString() }));
62
- const records = readPositionHistory();
63
- expect(records[0].symbol).toBe("THIRD");
64
- expect(records[1].symbol).toBe("SECOND");
65
- expect(records[2].symbol).toBe("FIRST");
66
- });
67
- it("should limit results", () => {
68
- for (let i = 0; i < 10; i++) {
69
- logPosition(makeRecord({ symbol: `SYM${i}` }));
70
- }
71
- const records = readPositionHistory({ limit: 3 });
72
- expect(records).toHaveLength(3);
73
- });
74
- });
75
- describe("readPositionHistory — filtering", () => {
76
- it("should filter by symbol", () => {
77
- logPosition(makeRecord({ symbol: "BTC" }));
78
- logPosition(makeRecord({ symbol: "ETH" }));
79
- logPosition(makeRecord({ symbol: "BTC-PERP" }));
80
- const records = readPositionHistory({ symbol: "BTC" });
81
- expect(records).toHaveLength(2);
82
- expect(records.every(r => r.symbol.includes("BTC"))).toBe(true);
83
- });
84
- it("should filter by exchange", () => {
85
- logPosition(makeRecord({ exchange: "hyperliquid" }));
86
- logPosition(makeRecord({ exchange: "pacifica" }));
87
- logPosition(makeRecord({ exchange: "hyperliquid" }));
88
- const records = readPositionHistory({ exchange: "hyperliquid" });
89
- expect(records).toHaveLength(2);
90
- expect(records.every(r => r.exchange === "hyperliquid")).toBe(true);
91
- });
92
- it("should filter by status", () => {
93
- logPosition(makeRecord({ status: "open" }));
94
- logPosition(makeRecord({ status: "closed", closedAt: new Date().toISOString() }));
95
- logPosition(makeRecord({ status: "updated" }));
96
- logPosition(makeRecord({ status: "closed", closedAt: new Date().toISOString() }));
97
- const records = readPositionHistory({ status: "closed" });
98
- expect(records).toHaveLength(2);
99
- expect(records.every(r => r.status === "closed")).toBe(true);
100
- });
101
- it("should filter by since date", () => {
102
- const old = new Date(Date.now() - 7 * 86400000).toISOString();
103
- const recent = new Date().toISOString();
104
- logPosition(makeRecord({ symbol: "OLD", updatedAt: old }));
105
- logPosition(makeRecord({ symbol: "NEW", updatedAt: recent }));
106
- const since = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
107
- const records = readPositionHistory({ since });
108
- expect(records).toHaveLength(1);
109
- expect(records[0].symbol).toBe("NEW");
110
- });
111
- it("should combine filters", () => {
112
- logPosition(makeRecord({ exchange: "hl", symbol: "BTC", status: "closed" }));
113
- logPosition(makeRecord({ exchange: "hl", symbol: "ETH", status: "closed" }));
114
- logPosition(makeRecord({ exchange: "pac", symbol: "BTC", status: "closed" }));
115
- logPosition(makeRecord({ exchange: "hl", symbol: "BTC", status: "open" }));
116
- const records = readPositionHistory({ exchange: "hl", symbol: "BTC", status: "closed" });
117
- expect(records).toHaveLength(1);
118
- expect(records[0].exchange).toBe("hl");
119
- expect(records[0].symbol).toBe("BTC");
120
- expect(records[0].status).toBe("closed");
121
- });
122
- });
123
- describe("readPositionHistory — edge cases", () => {
124
- it("should return empty array when file does not exist", () => {
125
- const records = readPositionHistory();
126
- expect(records).toEqual([]);
127
- });
128
- it("should handle empty file", () => {
129
- writeFileSync(POSITIONS_FILE, "", { mode: 0o600 });
130
- const records = readPositionHistory();
131
- expect(records).toEqual([]);
132
- });
133
- it("should skip malformed lines", () => {
134
- writeFileSync(POSITIONS_FILE, "not json\n" + JSON.stringify(makeRecord({ symbol: "OK" })) + "\n", { mode: 0o600 });
135
- const records = readPositionHistory();
136
- expect(records).toHaveLength(1);
137
- expect(records[0].symbol).toBe("OK");
138
- });
139
- });
140
- describe("getPositionStats", () => {
141
- it("should return zeroed stats when no data", () => {
142
- const stats = getPositionStats();
143
- expect(stats.totalTrades).toBe(0);
144
- expect(stats.wins).toBe(0);
145
- expect(stats.losses).toBe(0);
146
- expect(stats.winRate).toBe(0);
147
- expect(stats.totalPnl).toBe(0);
148
- expect(stats.avgPnl).toBe(0);
149
- expect(stats.bestTrade).toBe(0);
150
- expect(stats.worstTrade).toBe(0);
151
- expect(stats.shortestTrade).toBe(0);
152
- expect(stats.longestTrade).toBe(0);
153
- });
154
- it("should compute stats from closed positions", () => {
155
- // 3 closed trades: +100, -50, +200
156
- logPosition(makeRecord({
157
- status: "closed",
158
- realizedPnl: "100",
159
- exchange: "hl",
160
- symbol: "BTC",
161
- duration: 60000,
162
- }));
163
- logPosition(makeRecord({
164
- status: "closed",
165
- realizedPnl: "-50",
166
- exchange: "hl",
167
- symbol: "ETH",
168
- duration: 120000,
169
- }));
170
- logPosition(makeRecord({
171
- status: "closed",
172
- realizedPnl: "200",
173
- exchange: "pac",
174
- symbol: "BTC",
175
- duration: 30000,
176
- }));
177
- // Open positions should be ignored
178
- logPosition(makeRecord({
179
- status: "open",
180
- unrealizedPnl: "999",
181
- exchange: "hl",
182
- symbol: "SOL",
183
- }));
184
- const stats = getPositionStats();
185
- expect(stats.totalTrades).toBe(3);
186
- expect(stats.wins).toBe(2);
187
- expect(stats.losses).toBe(1);
188
- expect(stats.winRate).toBeCloseTo(66.67, 0);
189
- expect(stats.totalPnl).toBe(250);
190
- expect(stats.avgPnl).toBeCloseTo(83.33, 0);
191
- expect(stats.bestTrade).toBe(200);
192
- expect(stats.worstTrade).toBe(-50);
193
- expect(stats.avgDuration).toBeCloseTo(70000, -3);
194
- expect(stats.longestTrade).toBe(120000);
195
- expect(stats.shortestTrade).toBe(30000);
196
- });
197
- it("should compute bySymbol stats", () => {
198
- logPosition(makeRecord({ status: "closed", realizedPnl: "100", symbol: "BTC", duration: 1000 }));
199
- logPosition(makeRecord({ status: "closed", realizedPnl: "-30", symbol: "BTC", duration: 2000 }));
200
- logPosition(makeRecord({ status: "closed", realizedPnl: "50", symbol: "ETH", duration: 3000 }));
201
- const stats = getPositionStats();
202
- expect(stats.bySymbol.BTC.trades).toBe(2);
203
- expect(stats.bySymbol.BTC.pnl).toBe(70);
204
- expect(stats.bySymbol.BTC.winRate).toBe(50);
205
- expect(stats.bySymbol.ETH.trades).toBe(1);
206
- expect(stats.bySymbol.ETH.pnl).toBe(50);
207
- expect(stats.bySymbol.ETH.winRate).toBe(100);
208
- });
209
- it("should compute byExchange stats", () => {
210
- logPosition(makeRecord({ status: "closed", realizedPnl: "100", exchange: "hl", duration: 1000 }));
211
- logPosition(makeRecord({ status: "closed", realizedPnl: "-20", exchange: "pac", duration: 2000 }));
212
- logPosition(makeRecord({ status: "closed", realizedPnl: "50", exchange: "hl", duration: 3000 }));
213
- const stats = getPositionStats();
214
- expect(stats.byExchange.hl.trades).toBe(2);
215
- expect(stats.byExchange.hl.pnl).toBe(150);
216
- expect(stats.byExchange.pac.trades).toBe(1);
217
- expect(stats.byExchange.pac.pnl).toBe(-20);
218
- });
219
- it("should filter stats by exchange", () => {
220
- logPosition(makeRecord({ status: "closed", realizedPnl: "100", exchange: "hl", duration: 1000 }));
221
- logPosition(makeRecord({ status: "closed", realizedPnl: "-20", exchange: "pac", duration: 2000 }));
222
- const stats = getPositionStats({ exchange: "hl" });
223
- expect(stats.totalTrades).toBe(1);
224
- expect(stats.totalPnl).toBe(100);
225
- });
226
- });
227
- describe("attachPositionLogger", () => {
228
- it("should forward all events to the original callback", () => {
229
- const received = [];
230
- const originalOnEvent = (e) => received.push(e);
231
- const wrapped = attachPositionLogger(originalOnEvent);
232
- const heartbeat = {
233
- type: "heartbeat",
234
- exchange: "test",
235
- timestamp: new Date().toISOString(),
236
- data: { cycle: 1 },
237
- };
238
- wrapped(heartbeat);
239
- expect(received).toHaveLength(1);
240
- expect(received[0].type).toBe("heartbeat");
241
- });
242
- it("should log position_opened events", () => {
243
- const received = [];
244
- const wrapped = attachPositionLogger((e) => received.push(e));
245
- const event = {
246
- type: "position_opened",
247
- exchange: "test-ex",
248
- timestamp: new Date().toISOString(),
249
- data: { symbol: "BTC", side: "long", size: "0.5", entryPrice: "65000", unrealizedPnl: "0" },
250
- };
251
- wrapped(event);
252
- // Event should be forwarded
253
- expect(received).toHaveLength(1);
254
- // Position should be logged
255
- const positions = readPositionHistory({ exchange: "test-ex" });
256
- expect(positions).toHaveLength(1);
257
- expect(positions[0].symbol).toBe("BTC");
258
- expect(positions[0].side).toBe("long");
259
- expect(positions[0].status).toBe("open");
260
- expect(positions[0].entryPrice).toBe("65000");
261
- expect(positions[0].size).toBe("0.5");
262
- });
263
- it("should log position_updated events", () => {
264
- const wrapped = attachPositionLogger(() => { });
265
- // First open the position
266
- wrapped({
267
- type: "position_opened",
268
- exchange: "test-ex",
269
- timestamp: new Date().toISOString(),
270
- data: { symbol: "ETH", side: "short", size: "5", entryPrice: "2000", unrealizedPnl: "0" },
271
- });
272
- // Then update it
273
- wrapped({
274
- type: "position_updated",
275
- exchange: "test-ex",
276
- timestamp: new Date().toISOString(),
277
- data: { symbol: "ETH", side: "short", size: "10", entryPrice: "2000", unrealizedPnl: "-20", prevSize: "5", prevSide: "short" },
278
- });
279
- const positions = readPositionHistory({ status: "updated" });
280
- expect(positions).toHaveLength(1);
281
- expect(positions[0].size).toBe("10");
282
- expect(positions[0].status).toBe("updated");
283
- expect(positions[0].meta?.prevSize).toBe("5");
284
- });
285
- it("should log position_closed events with duration", () => {
286
- const wrapped = attachPositionLogger(() => { });
287
- const openTime = new Date("2026-03-08T10:00:00.000Z");
288
- const closeTime = new Date("2026-03-08T10:05:00.000Z");
289
- wrapped({
290
- type: "position_opened",
291
- exchange: "test-ex",
292
- timestamp: openTime.toISOString(),
293
- data: { symbol: "SOL", side: "long", size: "100", entryPrice: "150", unrealizedPnl: "0" },
294
- });
295
- wrapped({
296
- type: "position_closed",
297
- exchange: "test-ex",
298
- timestamp: closeTime.toISOString(),
299
- data: { symbol: "SOL", side: "long", size: "100", entryPrice: "150", unrealizedPnl: "50" },
300
- });
301
- const closed = readPositionHistory({ status: "closed" });
302
- expect(closed).toHaveLength(1);
303
- expect(closed[0].status).toBe("closed");
304
- expect(closed[0].realizedPnl).toBe("50");
305
- expect(closed[0].closedAt).toBe(closeTime.toISOString());
306
- expect(closed[0].duration).toBe(300000); // 5 minutes in ms
307
- });
308
- it("should track multiple positions independently", () => {
309
- const wrapped = attachPositionLogger(() => { });
310
- const ts = new Date().toISOString();
311
- wrapped({
312
- type: "position_opened",
313
- exchange: "ex",
314
- timestamp: ts,
315
- data: { symbol: "BTC", side: "long", size: "0.1", entryPrice: "65000", unrealizedPnl: "0" },
316
- });
317
- wrapped({
318
- type: "position_opened",
319
- exchange: "ex",
320
- timestamp: ts,
321
- data: { symbol: "ETH", side: "short", size: "5", entryPrice: "2000", unrealizedPnl: "0" },
322
- });
323
- // Close only BTC
324
- wrapped({
325
- type: "position_closed",
326
- exchange: "ex",
327
- timestamp: ts,
328
- data: { symbol: "BTC", side: "long", size: "0.1", entryPrice: "65000", unrealizedPnl: "100" },
329
- });
330
- const all = readPositionHistory({ exchange: "ex" });
331
- // 2 opens + 1 close = 3 records
332
- expect(all).toHaveLength(3);
333
- const closed = readPositionHistory({ exchange: "ex", status: "closed" });
334
- expect(closed).toHaveLength(1);
335
- expect(closed[0].symbol).toBe("BTC");
336
- });
337
- it("should handle position_closed without prior open gracefully", () => {
338
- const wrapped = attachPositionLogger(() => { });
339
- // Close without opening (e.g., logger started mid-session)
340
- wrapped({
341
- type: "position_closed",
342
- exchange: "test",
343
- timestamp: new Date().toISOString(),
344
- data: { symbol: "BTC", side: "long", size: "0.1", entryPrice: "65000", unrealizedPnl: "0" },
345
- });
346
- const closed = readPositionHistory({ status: "closed" });
347
- expect(closed).toHaveLength(1);
348
- // Duration should be undefined since we don't know when it opened
349
- });
350
- it("should not log non-position events", () => {
351
- const wrapped = attachPositionLogger(() => { });
352
- wrapped({
353
- type: "order_placed",
354
- exchange: "test",
355
- timestamp: new Date().toISOString(),
356
- data: { orderId: "o1", symbol: "BTC" },
357
- });
358
- wrapped({
359
- type: "balance_update",
360
- exchange: "test",
361
- timestamp: new Date().toISOString(),
362
- data: { equity: "1000" },
363
- });
364
- const positions = readPositionHistory();
365
- expect(positions).toHaveLength(0);
366
- });
367
- });