perp-cli 0.11.0 → 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 (240) 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/bridge-engine.js +102 -49
  21. package/dist/cli-spec.js +1 -2
  22. package/dist/commands/account.js +2 -500
  23. package/dist/commands/agent.d.ts +8 -2
  24. package/dist/commands/agent.js +1903 -366
  25. package/dist/commands/alerts.js +3 -1
  26. package/dist/commands/arb/index.js +37 -7
  27. package/dist/commands/arb-auto.js +44 -11
  28. package/dist/commands/arb.js +83 -34
  29. package/dist/commands/bot.d.ts +1 -1
  30. package/dist/commands/bot.js +18 -14
  31. package/dist/commands/bridge.d.ts +1 -1
  32. package/dist/commands/bridge.js +4 -4
  33. package/dist/commands/funds.d.ts +1 -1
  34. package/dist/commands/funds.js +18 -68
  35. package/dist/commands/jobs.d.ts +1 -1
  36. package/dist/commands/jobs.js +2 -2
  37. package/dist/commands/manage.d.ts +1 -1
  38. package/dist/commands/manage.js +12 -84
  39. package/dist/commands/plan.d.ts +1 -1
  40. package/dist/commands/plan.js +2 -2
  41. package/dist/commands/portfolio.d.ts +19 -3
  42. package/dist/commands/portfolio.js +747 -163
  43. package/dist/commands/rebalance.d.ts +1 -1
  44. package/dist/commands/rebalance.js +18 -12
  45. package/dist/commands/risk.js +2 -6
  46. package/dist/commands/settings.js +30 -53
  47. package/dist/commands/trade.js +16 -15
  48. package/dist/commands/wallet.d.ts +2 -1
  49. package/dist/commands/wallet.js +756 -28
  50. package/dist/errors.d.ts +64 -0
  51. package/dist/errors.js +82 -2
  52. package/dist/event-stream.js +12 -1
  53. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  54. package/dist/exchanges/aster-typed-data.js +189 -0
  55. package/dist/exchanges/aster.d.ts +60 -11
  56. package/dist/exchanges/aster.js +278 -136
  57. package/dist/exchanges/hyperliquid.d.ts +14 -0
  58. package/dist/exchanges/hyperliquid.js +84 -34
  59. package/dist/exchanges/lighter-spot.js +14 -3
  60. package/dist/exchanges/lighter.d.ts +66 -0
  61. package/dist/exchanges/lighter.js +199 -27
  62. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  63. package/dist/exchanges/pacifica-typed-data.js +74 -0
  64. package/dist/exchanges/pacifica.d.ts +29 -1
  65. package/dist/exchanges/pacifica.js +94 -6
  66. package/dist/funding/rates.js +59 -21
  67. package/dist/guardrail/perp-guardrail.js +9 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +255 -360
  70. package/dist/jobs.js +3 -1
  71. package/dist/mcp-server.js +85 -83
  72. package/dist/position-history.js +47 -4
  73. package/dist/settings.d.ts +67 -3
  74. package/dist/settings.js +1 -2
  75. package/dist/signer/ows-evm.d.ts +14 -0
  76. package/dist/signer/ows-evm.js +28 -9
  77. package/dist/smart-order.d.ts +8 -2
  78. package/dist/smart-order.js +46 -11
  79. package/dist/strategies/funding-arb.js +29 -8
  80. package/dist/trade-validator.js +30 -5
  81. package/dist/utils.d.ts +10 -0
  82. package/dist/utils.js +19 -0
  83. package/package.json +2 -2
  84. package/skills/perp-cli/SKILL.md +23 -14
  85. package/skills/perp-cli/references/agent-operations.md +6 -4
  86. package/skills/perp-cli/references/commands.md +55 -34
  87. package/skills/perp-cli/scripts/preflight.sh +4 -4
  88. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  89. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  90. package/dist/__tests__/alert-logic.test.js +0 -107
  91. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  92. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  93. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  94. package/dist/__tests__/arb-history-stats.test.js +0 -176
  95. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  96. package/dist/__tests__/arb-logic.test.js +0 -84
  97. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  98. package/dist/__tests__/arb-manage.test.js +0 -253
  99. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  100. package/dist/__tests__/arb-new-features.test.js +0 -457
  101. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  102. package/dist/__tests__/arb-sizing.test.js +0 -48
  103. package/dist/__tests__/arb-state.test.d.ts +0 -1
  104. package/dist/__tests__/arb-state.test.js +0 -284
  105. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  106. package/dist/__tests__/arb-userflow.test.js +0 -945
  107. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  108. package/dist/__tests__/arb-utils.test.js +0 -264
  109. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  110. package/dist/__tests__/bot-conditions.test.js +0 -341
  111. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  112. package/dist/__tests__/client-id-tracker.test.js +0 -137
  113. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  114. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  115. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  116. package/dist/__tests__/commands/order-intent.test.js +0 -600
  117. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  118. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  119. package/dist/__tests__/config.test.d.ts +0 -1
  120. package/dist/__tests__/config.test.js +0 -86
  121. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  122. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  123. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  124. package/dist/__tests__/dex-asset-map.test.js +0 -191
  125. package/dist/__tests__/errors.test.d.ts +0 -1
  126. package/dist/__tests__/errors.test.js +0 -110
  127. package/dist/__tests__/event-stream.test.d.ts +0 -1
  128. package/dist/__tests__/event-stream.test.js +0 -276
  129. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  130. package/dist/__tests__/exchanges/interface.test.js +0 -132
  131. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  132. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  133. package/dist/__tests__/execution-log.test.d.ts +0 -1
  134. package/dist/__tests__/execution-log.test.js +0 -106
  135. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  136. package/dist/__tests__/funding-calc.test.js +0 -71
  137. package/dist/__tests__/funding-history.test.d.ts +0 -1
  138. package/dist/__tests__/funding-history.test.js +0 -343
  139. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  140. package/dist/__tests__/funding-rates.test.js +0 -342
  141. package/dist/__tests__/funding.test.d.ts +0 -1
  142. package/dist/__tests__/funding.test.js +0 -173
  143. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  144. package/dist/__tests__/gap-logic.test.js +0 -43
  145. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  146. package/dist/__tests__/hip3-dex.test.js +0 -234
  147. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  148. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  149. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  150. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  151. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  152. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  153. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  154. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  155. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  156. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  157. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  158. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  159. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  160. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  161. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  162. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  163. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  165. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  166. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  167. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  169. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  170. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  171. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  173. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  175. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  177. package/dist/__tests__/liquidity.test.d.ts +0 -1
  178. package/dist/__tests__/liquidity.test.js +0 -225
  179. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  180. package/dist/__tests__/plan-executor.test.js +0 -314
  181. package/dist/__tests__/position-history.test.d.ts +0 -1
  182. package/dist/__tests__/position-history.test.js +0 -367
  183. package/dist/__tests__/retry.test.d.ts +0 -1
  184. package/dist/__tests__/retry.test.js +0 -310
  185. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  186. package/dist/__tests__/risk-assessment.test.js +0 -288
  187. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  188. package/dist/__tests__/security-adversarial.test.js +0 -574
  189. package/dist/__tests__/strategies.test.d.ts +0 -1
  190. package/dist/__tests__/strategies.test.js +0 -539
  191. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  192. package/dist/__tests__/trade-execution.test.js +0 -129
  193. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  194. package/dist/__tests__/trade-validator.test.js +0 -655
  195. package/dist/__tests__/utils.test.d.ts +0 -1
  196. package/dist/__tests__/utils.test.js +0 -76
  197. package/dist/arb/reconcile.d.ts +0 -28
  198. package/dist/arb/reconcile.js +0 -162
  199. package/dist/commands/alert.d.ts +0 -2
  200. package/dist/commands/alert.js +0 -421
  201. package/dist/commands/analytics.d.ts +0 -3
  202. package/dist/commands/analytics.js +0 -311
  203. package/dist/commands/arb-manage.d.ts +0 -5
  204. package/dist/commands/arb-manage.js +0 -5
  205. package/dist/commands/dashboard.d.ts +0 -3
  206. package/dist/commands/dashboard.js +0 -176
  207. package/dist/commands/deposit.d.ts +0 -4
  208. package/dist/commands/deposit.js +0 -573
  209. package/dist/commands/dex.d.ts +0 -3
  210. package/dist/commands/dex.js +0 -120
  211. package/dist/commands/env.d.ts +0 -2
  212. package/dist/commands/env.js +0 -136
  213. package/dist/commands/funding.d.ts +0 -3
  214. package/dist/commands/funding.js +0 -554
  215. package/dist/commands/gap.d.ts +0 -2
  216. package/dist/commands/gap.js +0 -305
  217. package/dist/commands/health.d.ts +0 -2
  218. package/dist/commands/health.js +0 -67
  219. package/dist/commands/multileg.d.ts +0 -16
  220. package/dist/commands/multileg.js +0 -174
  221. package/dist/commands/ows.d.ts +0 -2
  222. package/dist/commands/ows.js +0 -887
  223. package/dist/commands/pnl.d.ts +0 -12
  224. package/dist/commands/pnl.js +0 -243
  225. package/dist/commands/run.d.ts +0 -3
  226. package/dist/commands/run.js +0 -202
  227. package/dist/commands/stream.d.ts +0 -5
  228. package/dist/commands/stream.js +0 -200
  229. package/dist/commands/withdraw.d.ts +0 -3
  230. package/dist/commands/withdraw.js +0 -187
  231. package/dist/exchanges/edgex.d.ts +0 -62
  232. package/dist/exchanges/edgex.js +0 -552
  233. package/dist/exchanges/nado.d.ts +0 -52
  234. package/dist/exchanges/nado.js +0 -223
  235. package/dist/funding/index.d.ts +0 -3
  236. package/dist/funding/index.js +0 -3
  237. package/dist/retry.d.ts +0 -74
  238. package/dist/retry.js +0 -129
  239. package/dist/ws/market-feed.d.ts +0 -74
  240. package/dist/ws/market-feed.js +0 -227
package/dist/index.js CHANGED
@@ -1,12 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ // MUST be first import — installs a stderr filter for --json mode so SDK
3
+ // startup banners don't leak into machine output. ESM hoists all static
4
+ // imports to the top, but executes them in source order, so this side-effect
5
+ // import runs before any DEX SDK is loaded.
6
+ import "./_stderr-filter.js";
2
7
  // Save original process.exit before any SDK (Go WASM) can patch it
3
8
  const _origExit = process.exit.bind(process);
4
9
  import { config } from "dotenv";
5
10
  import { resolve } from "path";
6
11
  import { createRequire } from "node:module";
7
- // Load ~/.perp/.env first (global), then CWD .env (overrides)
12
+ // SSOT Rule #3: load only ~/.perp/.env. The CWD .env auto-load was removed
13
+ // because it caused master-address pollution when run from a project tree
14
+ // containing leftover dev keys (`wallet show` returned env-derived addresses
15
+ // instead of the OWS active wallet's). If you need ad-hoc env overrides,
16
+ // export them in your shell — keystore is the canonical source.
8
17
  config({ path: resolve(process.env.HOME || "~", ".perp", ".env") });
9
- config();
10
18
  import { Command } from "commander";
11
19
  import chalk from "chalk";
12
20
  import { Keypair } from "@solana/web3.js";
@@ -17,28 +25,26 @@ import { resolveExchangeName } from "./exchanges/registry.js";
17
25
  import { registerMarketCommands } from "./commands/market.js";
18
26
  import { registerAccountCommands } from "./commands/account.js";
19
27
  import { registerTradeCommands } from "./commands/trade.js";
20
- import { registerManageCommands } from "./commands/manage.js";
28
+ // manage commands now register under `wallet manage ...` via registerWalletCommands.
21
29
  // stream commands removed — WS feeds still used by dashboard/event-stream internally
22
30
  import { registerArbCommands } from "./commands/arb.js";
23
31
  import { registerWalletCommands } from "./commands/wallet.js";
24
- import { registerOwsCommands } from "./commands/ows.js";
25
- import { registerBridgeCommands } from "./commands/bridge.js";
26
- // deposit + withdraw merged into funds
32
+ // bridge + rebalance now nested under `funds` — registered inside registerFundsCommands.
27
33
  import { registerFundsCommands } from "./commands/funds.js";
28
34
  // alert commands removed
29
35
  import { registerArbAutoCommands } from "./commands/arb-auto.js";
30
36
  import { registerArbManageCommands } from "./commands/arb/index.js";
31
- import { registerAgentCommands } from "./commands/agent.js";
32
- import { registerRebalanceCommands } from "./commands/rebalance.js";
33
- import { registerBotCommands } from "./commands/bot.js";
37
+ // Agent commands now register under `wallet agent ...` via registerWalletCommands.
38
+ import { registerStrategyCommands } from "./commands/bot.js";
34
39
  import { registerRiskCommands } from "./commands/risk.js";
35
40
  import { registerHistoryCommands } from "./commands/history.js";
36
41
  import { registerSettingsCommands } from "./commands/settings.js";
37
42
  // dex commands merged into market (hip3) — use --dex flag for markets/balance
38
- import { registerPlanCommands } from "./commands/plan.js";
43
+ // `plan` is no longer top-level — registered as `strategy plan` inside
44
+ // registerStrategyCommands.
39
45
  // funding merged into arb.ts
40
46
  import { registerBacktestCommands } from "./commands/backtest.js";
41
- import { registerDashboardCommands } from "./commands/dashboard.js";
47
+ import { registerPortfolioCommand } from "./commands/portfolio.js";
42
48
  import { registerInitCommand } from "./commands/init.js";
43
49
  import { registerAlertCommands } from "./commands/alerts.js";
44
50
  import { loadSettings, saveSettings } from "./settings.js";
@@ -63,6 +69,8 @@ program
63
69
  .option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
64
70
  .option("--ows <name>", "Use an OWS (Open Wallet Standard) wallet by name")
65
71
  .option("--ows-key <token>", "OWS API key token (ows_key_...) for policy-gated agent access")
72
+ .option("--no-agent", "Bypass agent wallet routing for this run; falls back to OWS master or PK direct")
73
+ .option("--passphrase <pp>", "Master OWS passphrase (also accepts OWS_PASSPHRASE env or stdin)")
66
74
  .option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
67
75
  .configureOutput({
68
76
  writeErr: (str) => {
@@ -96,6 +104,9 @@ async function getAdapter() {
96
104
  const owsName = opts.ows;
97
105
  const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
98
106
  const walletName = owsName ? `ows:${owsName}` : opts.wallet;
107
+ // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
108
+ // Normalize so downstream wiring can use a single boolean consistently.
109
+ const noAgent = opts.agent === false;
99
110
  // Skip cache when --wallet/--ows is specified (different wallet = different account)
100
111
  if (!walletName && _adapter)
101
112
  return _adapter;
@@ -115,6 +126,27 @@ async function getAdapter() {
115
126
  const settings = loadSettings();
116
127
  const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
117
128
  _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
129
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet (Phase 2c)
130
+ const pacOwsName = owsName || _settings.owsActiveWallet;
131
+ if (pacOwsName && !pk) {
132
+ const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
133
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
134
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
135
+ if (pp !== null)
136
+ _pacificaAdapter.setSigner(OwsSolanaSigner.create(pacOwsName, pp));
137
+ }
138
+ // Tier 1: agent if registered (Phase 2c)
139
+ const { getAgent: getPacAgent } = await import("./agent-wallet/store.js");
140
+ const pacAgentMeta = getPacAgent("pacifica");
141
+ if (pacAgentMeta) {
142
+ const { OwsSolanaSigner: PacOwsSolanaSigner } = await import("./signer/ows-solana.js");
143
+ // Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
144
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
145
+ const agentSigner = PacOwsSolanaSigner.create(pacAgentMeta.agentWalletName, "");
146
+ _pacificaAdapter.setAgentSigner(pacAgentMeta, agentSigner);
147
+ }
148
+ if (noAgent)
149
+ _pacificaAdapter.setNoAgent(true);
118
150
  _adapter = _pacificaAdapter;
119
151
  break;
120
152
  }
@@ -125,43 +157,79 @@ async function getAdapter() {
125
157
  await _hlAdapter.init();
126
158
  if (pk) {
127
159
  const hlSettings = loadSettings();
128
- if (hlSettings.referrals && !hlSettings.referralApplied.hyperliquid) {
160
+ if (!hlSettings.referralApplied.hyperliquid) {
129
161
  const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
130
162
  if (hlRef) {
131
163
  _hlAdapter.autoSetReferrer(hlRef).then(() => {
132
164
  const s = loadSettings();
133
165
  s.referralApplied.hyperliquid = true;
134
166
  saveSettings(s);
135
- }).catch(() => {
136
- const s = loadSettings();
137
- s.referralApplied.hyperliquid = true;
138
- saveSettings(s);
167
+ }).catch((err) => {
168
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
169
+ // Leave referralApplied[ex]=false so next adapter init retries.
170
+ process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
139
171
  });
140
172
  }
141
173
  }
142
174
  }
175
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet
176
+ const hlOwsName = owsName || _settings.owsActiveWallet;
177
+ if (hlOwsName && !pk) {
178
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
179
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
180
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
181
+ if (pp !== null)
182
+ _hlAdapter.setSigner(OwsEvmSigner.create(hlOwsName, pp));
183
+ }
184
+ // Tier 1: agent if registered
185
+ const { getAgent: getHlAgent } = await import("./agent-wallet/store.js");
186
+ const hlAgentMeta = getHlAgent("hyperliquid");
187
+ if (hlAgentMeta) {
188
+ const { OwsEvmSigner: HlOwsEvmSigner } = await import("./signer/ows-evm.js");
189
+ // HL agent wallet was created with empty passphrase (see runHlApproveFlow);
190
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
191
+ const agentSigner = HlOwsEvmSigner.create(hlAgentMeta.agentWalletName, "");
192
+ _hlAdapter.setAgentSigner(hlAgentMeta, agentSigner);
193
+ }
194
+ if (noAgent)
195
+ _hlAdapter.setNoAgent(true);
143
196
  _adapter = _hlAdapter;
144
197
  break;
145
198
  }
146
199
  case "lighter": {
147
200
  const { LighterAdapter } = await import("./exchanges/lighter.js");
201
+ // Phase 2d (SSOT Rule #3): when an agent is registered AND not bypassed,
202
+ // load the agent's L2 secp256k1 key from the encrypted keystore at
203
+ // ~/.perp/lighter-agents/<accountIndex>-<slot>.json BEFORE init() so the
204
+ // WASM client is created with the agent identity. Missing keystore →
205
+ // loadLighterKey throws LIGHTER_KEYSTORE_NOT_FOUND with remediation
206
+ // (no fallback to env per SSOT Rule #2).
207
+ const { getAgent: getLtAgent } = await import("./agent-wallet/store.js");
208
+ const ltAgentMeta = getLtAgent("lighter");
148
209
  _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
210
+ if (ltAgentMeta && !noAgent) {
211
+ const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
212
+ const ltL2Key = loadLighterKey(ltAgentMeta.accountIndex, ltAgentMeta.apiKeyIndex, "");
213
+ _lighterAdapter.setAgentSigner(ltAgentMeta, ltL2Key);
214
+ }
215
+ if (noAgent)
216
+ _lighterAdapter.setNoAgent(true);
149
217
  await _lighterAdapter.init();
150
- if (pk) {
151
- const ltSettings = loadSettings();
152
- if (ltSettings.referrals && !ltSettings.referralApplied.lighter) {
153
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
154
- if (ltRef) {
155
- _lighterAdapter.useReferralCode(ltRef).then(() => {
156
- const s = loadSettings();
157
- s.referralApplied.lighter = true;
158
- saveSettings(s);
159
- }).catch(() => {
160
- const s = loadSettings();
161
- s.referralApplied.lighter = true;
162
- saveSettings(s);
163
- });
164
- }
218
+ // LT referral apply is L2-signed (POST /referral/use uses the WASM slot
219
+ // signer's auth token, not the master EVM). Trigger on any active
220
+ // signer tier — agent / OWS master / PK direct — gated by isReadOnly.
221
+ const ltSettings = loadSettings();
222
+ if (!ltSettings.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
223
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
224
+ if (ltRef) {
225
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
226
+ const s = loadSettings();
227
+ s.referralApplied.lighter = true;
228
+ saveSettings(s);
229
+ }).catch((err) => {
230
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
231
+ process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
232
+ });
165
233
  }
166
234
  }
167
235
  _adapter = _lighterAdapter;
@@ -169,8 +237,27 @@ async function getAdapter() {
169
237
  }
170
238
  case "aster": {
171
239
  const { AsterAdapter } = await import("./exchanges/aster.js");
172
- const ast = new AsterAdapter(undefined, undefined, isTestnet);
240
+ const ast = new AsterAdapter(pk ?? undefined, isTestnet); // Tier 3 from PK
173
241
  await ast.init();
242
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet
243
+ const asterOwsName = owsName || _settings.owsActiveWallet;
244
+ if (asterOwsName) {
245
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
246
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
247
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
248
+ if (pp !== null)
249
+ ast.setMasterSigner(OwsEvmSigner.create(asterOwsName, pp));
250
+ }
251
+ // Tier 1: agent if registered
252
+ const { getAgent: getAsterAgent } = await import("./agent-wallet/store.js");
253
+ const agentMeta = getAsterAgent("aster");
254
+ if (agentMeta) {
255
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
256
+ const strat = agentSigningStrategyFor(agentMeta, owsKeyToken ?? "");
257
+ ast.setAgent(agentMeta, strat);
258
+ }
259
+ if (noAgent)
260
+ ast.setNoAgent(true);
174
261
  _adapter = ast;
175
262
  break;
176
263
  }
@@ -185,6 +272,8 @@ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToke
185
272
  const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
186
273
  // If an OWS API key token is provided, use it as passphrase → routes through policy engine
187
274
  const passphrase = owsKeyToken || "";
275
+ // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
276
+ const noAgent = opts.agent === false;
188
277
  switch (exchange) {
189
278
  case "pacifica": {
190
279
  const pacNetwork = (isTestnet ? "testnet" : "mainnet");
@@ -193,6 +282,15 @@ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToke
193
282
  const dummyKeypair = Keypair.generate();
194
283
  _pacificaAdapter = new PacificaAdapter(dummyKeypair, pacNetwork, builderCode, true);
195
284
  _pacificaAdapter.setSigner(OwsSolanaSigner.create(owsWalletName, passphrase));
285
+ // Tier 1: agent if registered (Phase 2c)
286
+ const { getAgent: getPacAgentOws } = await import("./agent-wallet/store.js");
287
+ const pacAgentMetaOws = getPacAgentOws("pacifica");
288
+ if (pacAgentMetaOws) {
289
+ const agentSignerOws = OwsSolanaSigner.create(pacAgentMetaOws.agentWalletName, "");
290
+ _pacificaAdapter.setAgentSigner(pacAgentMetaOws, agentSignerOws);
291
+ }
292
+ if (noAgent)
293
+ _pacificaAdapter.setNoAgent(true);
196
294
  _adapter = _pacificaAdapter;
197
295
  return _adapter;
198
296
  }
@@ -213,6 +311,23 @@ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToke
213
311
  _adapter = _lighterAdapter;
214
312
  return _adapter;
215
313
  }
314
+ case "aster": {
315
+ const { AsterAdapter } = await import("./exchanges/aster.js");
316
+ const asterOws = new AsterAdapter(undefined, isTestnet);
317
+ await asterOws.init();
318
+ asterOws.setMasterSigner(OwsEvmSigner.create(owsWalletName, passphrase));
319
+ // Optionally also set agent if registered
320
+ const { getAgent: getAsterAgentOws } = await import("./agent-wallet/store.js");
321
+ const asterMeta = getAsterAgentOws("aster");
322
+ if (asterMeta) {
323
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
324
+ asterOws.setAgent(asterMeta, agentSigningStrategyFor(asterMeta, owsKeyToken ?? ""));
325
+ }
326
+ if (noAgent)
327
+ asterOws.setNoAgent(true);
328
+ _adapter = asterOws;
329
+ return _adapter;
330
+ }
216
331
  default:
217
332
  throw new Error(`Unknown exchange: ${exchange}`);
218
333
  }
@@ -244,13 +359,11 @@ function getHLAdapter() {
244
359
  registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange);
245
360
  registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
246
361
  registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
247
- registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
362
+ // manage tree wired inside registerWalletCommands below.
248
363
  // stream commands removed
249
364
  registerArbCommands(program, isJson, getAdapterForExchange);
250
- registerWalletCommands(program, isJson);
251
- registerOwsCommands(program, isJson);
252
- registerBridgeCommands(program, isJson);
253
- registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
365
+ registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter);
366
+ registerFundsCommands(program, getAdapter, isJson, () => program.opts().network, getAdapterForExchange);
254
367
  // alert commands removed
255
368
  // Helper to get adapter for a specific exchange (used by arb-auto)
256
369
  async function getAdapterForExchange(rawExchange) {
@@ -258,8 +371,19 @@ async function getAdapterForExchange(rawExchange) {
258
371
  const opts = program.opts();
259
372
  const network = opts.network;
260
373
  const isTestnet = network === "testnet";
261
- const walletName = opts.wallet;
374
+ const owsName = opts.ows;
375
+ const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
376
+ const walletName = owsName ? `ows:${owsName}` : opts.wallet;
377
+ // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
378
+ const noAgent = opts.agent === false;
262
379
  const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
380
+ // Route through OWS init when pk is an OWS reference (e.g. "ows:main").
381
+ // Mirrors getAdapter() at the top of this file. Without this, adapters
382
+ // would try to parse "ows:main" as a raw private key and ethers would
383
+ // throw "invalid BytesLike value" / parseSolanaKeypair would fail.
384
+ if (pk && isOwsKey(pk)) {
385
+ return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
386
+ }
263
387
  switch (exchange) {
264
388
  case "pacifica": {
265
389
  if (_pacificaAdapter)
@@ -269,6 +393,18 @@ async function getAdapterForExchange(rawExchange) {
269
393
  const s1 = loadSettings();
270
394
  const builderCode = process.env.PACIFICA_BUILDER_CODE || s1.referralCodes.pacifica || "PERPCLI";
271
395
  _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
396
+ // Tier 1: agent if registered (Phase 2c)
397
+ const { getAgent: getPacAgentEx } = await import("./agent-wallet/store.js");
398
+ const pacAgentMetaEx = getPacAgentEx("pacifica");
399
+ if (pacAgentMetaEx) {
400
+ const { OwsSolanaSigner: PacOwsSolanaSignerEx } = await import("./signer/ows-solana.js");
401
+ // Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
402
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
403
+ const agentSignerEx = PacOwsSolanaSignerEx.create(pacAgentMetaEx.agentWalletName, "");
404
+ _pacificaAdapter.setAgentSigner(pacAgentMetaEx, agentSignerEx);
405
+ }
406
+ if (noAgent)
407
+ _pacificaAdapter.setNoAgent(true);
272
408
  if (!_adapter)
273
409
  _adapter = _pacificaAdapter;
274
410
  return _pacificaAdapter;
@@ -282,21 +418,35 @@ async function getAdapterForExchange(rawExchange) {
282
418
  await _hlAdapter.init();
283
419
  if (pk) {
284
420
  const s2 = loadSettings();
285
- if (s2.referrals && !s2.referralApplied.hyperliquid) {
421
+ if (!s2.referralApplied.hyperliquid) {
286
422
  const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
287
423
  if (hlRef) {
288
424
  _hlAdapter.autoSetReferrer(hlRef).then(() => {
289
425
  const s = loadSettings();
290
426
  s.referralApplied.hyperliquid = true;
291
427
  saveSettings(s);
292
- }).catch(() => {
293
- const s = loadSettings();
294
- s.referralApplied.hyperliquid = true;
295
- saveSettings(s);
428
+ }).catch((err) => {
429
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
430
+ // Leave referralApplied[ex]=false so next adapter init retries.
431
+ process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
296
432
  });
297
433
  }
298
434
  }
299
435
  }
436
+ // Tier 1: agent if registered
437
+ {
438
+ const { getAgent: getHlAgent2 } = await import("./agent-wallet/store.js");
439
+ const hlAgentMeta2 = getHlAgent2("hyperliquid");
440
+ if (hlAgentMeta2) {
441
+ const { OwsEvmSigner: HlOwsEvmSigner2 } = await import("./signer/ows-evm.js");
442
+ // HL agent wallet was created with empty passphrase (see runHlApproveFlow);
443
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
444
+ const agentSigner2 = HlOwsEvmSigner2.create(hlAgentMeta2.agentWalletName, "");
445
+ _hlAdapter.setAgentSigner(hlAgentMeta2, agentSigner2);
446
+ }
447
+ if (noAgent)
448
+ _hlAdapter.setNoAgent(true);
449
+ }
300
450
  if (!_adapter)
301
451
  _adapter = _hlAdapter;
302
452
  return _hlAdapter;
@@ -305,23 +455,32 @@ async function getAdapterForExchange(rawExchange) {
305
455
  if (_lighterAdapter)
306
456
  return _lighterAdapter;
307
457
  const { LighterAdapter } = await import("./exchanges/lighter.js");
458
+ const { getAgent: getLtAgentEx } = await import("./agent-wallet/store.js");
459
+ const ltAgentMetaEx = getLtAgentEx("lighter");
308
460
  _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
461
+ if (ltAgentMetaEx && !noAgent) {
462
+ // SSOT Rule #3: load agent L2 key from encrypted keystore — no env fallback.
463
+ const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
464
+ const ltL2KeyEx = loadLighterKey(ltAgentMetaEx.accountIndex, ltAgentMetaEx.apiKeyIndex, "");
465
+ _lighterAdapter.setAgentSigner(ltAgentMetaEx, ltL2KeyEx);
466
+ }
467
+ if (noAgent)
468
+ _lighterAdapter.setNoAgent(true);
309
469
  await _lighterAdapter.init();
310
- if (pk) {
311
- const s3 = loadSettings();
312
- if (s3.referrals && !s3.referralApplied.lighter) {
313
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
314
- if (ltRef) {
315
- _lighterAdapter.useReferralCode(ltRef).then(() => {
316
- const s = loadSettings();
317
- s.referralApplied.lighter = true;
318
- saveSettings(s);
319
- }).catch(() => {
320
- const s = loadSettings();
321
- s.referralApplied.lighter = true;
322
- saveSettings(s);
323
- });
324
- }
470
+ // LT referral apply is L2-signed; trigger on any active tier (agent
471
+ // included), gated by isReadOnly. See first LT case for rationale.
472
+ const s3 = loadSettings();
473
+ if (!s3.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
474
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
475
+ if (ltRef) {
476
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
477
+ const s = loadSettings();
478
+ s.referralApplied.lighter = true;
479
+ saveSettings(s);
480
+ }).catch((err) => {
481
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
482
+ process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
483
+ });
325
484
  }
326
485
  }
327
486
  if (!_adapter)
@@ -330,9 +489,30 @@ async function getAdapterForExchange(rawExchange) {
330
489
  }
331
490
  case "aster": {
332
491
  const { AsterAdapter } = await import("./exchanges/aster.js");
333
- const ast = new AsterAdapter(undefined, undefined, isTestnet);
334
- await ast.init();
335
- return ast;
492
+ const astEx = new AsterAdapter(pk ?? undefined, isTestnet);
493
+ await astEx.init();
494
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet
495
+ const asterExOwsName = opts.ows || _settings.owsActiveWallet;
496
+ if (asterExOwsName) {
497
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
498
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
499
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
500
+ if (pp !== null)
501
+ astEx.setMasterSigner(OwsEvmSigner.create(asterExOwsName, pp));
502
+ }
503
+ // Tier 1: agent if registered
504
+ const owsKeyEx = opts.owsKey || process.env.OWS_API_KEY;
505
+ const { getAgent: getAsterAgentEx } = await import("./agent-wallet/store.js");
506
+ const agentMetaEx = getAsterAgentEx("aster");
507
+ if (agentMetaEx) {
508
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
509
+ astEx.setAgent(agentMetaEx, agentSigningStrategyFor(agentMetaEx, owsKeyEx ?? ""));
510
+ }
511
+ if (noAgent)
512
+ astEx.setNoAgent(true);
513
+ if (!_adapter)
514
+ _adapter = astEx;
515
+ return astEx;
336
516
  }
337
517
  default:
338
518
  throw new Error(`Unknown exchange: ${exchange}`);
@@ -355,312 +535,27 @@ async function getHLAdapterForDex(dex) {
355
535
  }
356
536
  registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
357
537
  registerArbManageCommands(program, getAdapterForExchange, isJson);
358
- registerAgentCommands(program, getAdapter, isJson);
359
- // withdraw merged into funds
360
- registerRebalanceCommands(program, getAdapterForExchange, isJson);
361
- // Jobs & strategies
362
- import { registerJobsCommands } from "./commands/jobs.js";
363
- registerJobsCommands(program, isJson);
364
- registerBotCommands(program, getAdapter, getAdapterForExchange, isJson);
538
+ // `agent` is no longer top-level — registered as `wallet agent` inside
539
+ // registerWalletCommands above.
540
+ // withdraw merged into funds.
541
+ // `rebalance` is no longer top-level — registered as `funds rebalance` inside
542
+ // registerFundsCommands above.
543
+ // Background processes & strategies
544
+ import { registerBackgroundCommands } from "./commands/jobs.js";
545
+ registerBackgroundCommands(program, isJson);
546
+ registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
365
547
  // Agent-friendly commands
366
548
  registerRiskCommands(program, getAdapterForExchange, isJson);
367
549
  registerHistoryCommands(program, isJson, getAdapterForExchange);
368
550
  registerSettingsCommands(program, isJson, getAdapterForExchange);
369
551
  // dex commands removed — use 'market hip3' + --dex flag
370
- registerPlanCommands(program, getAdapter, isJson);
552
+ // `plan` is no longer top-level — registered as `strategy plan` inside
553
+ // registerStrategyCommands above.
371
554
  // funding merged into arb — registerFundingCommands removed
372
555
  registerBacktestCommands(program, isJson);
373
- registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
556
+ registerPortfolioCommand(program, getAdapterForExchange, isJson, _pkg.version, getHLAdapterForDex);
374
557
  registerInitCommand(program);
375
558
  registerAlertCommands(program, isJson);
376
- // Agent discovery: perp api-spec — deprecated, use 'perp agent schema'
377
- const apiSpecCmd = program
378
- .command("api-spec")
379
- .description("Use 'perp agent schema'")
380
- .action(async () => {
381
- const { jsonOk, printJson } = await import("./utils.js");
382
- const { getCliSpec } = await import("./cli-spec.js");
383
- printJson(jsonOk(getCliSpec(program)));
384
- });
385
- apiSpecCmd._hidden = true;
386
- // Unified dashboard: balances + positions + top arb opportunities
387
- program.command("status")
388
- .description("Unified dashboard: balances, positions, and top arb opportunities")
389
- .option("--health", "Check connectivity only")
390
- .action(async (opts) => {
391
- if (opts.health) {
392
- const { runHealthCheck } = await import("./commands/risk.js");
393
- return runHealthCheck(isJson);
394
- }
395
- const json = isJson();
396
- const { formatUsd, formatPnl, makeTable, printJson, jsonOk, withJsonErrors } = await import("./utils.js");
397
- const { fetchAllFundingRates, TOP_SYMBOLS } = await import("./funding-rates.js");
398
- const { saveFundingSnapshot, getHistoricalRates } = await import("./funding-history.js");
399
- await withJsonErrors(json, async () => {
400
- const EX_LIST = ["pacifica", "hyperliquid", "lighter", "aster"];
401
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : "AST";
402
- const snapshotPromises = EX_LIST.map(async (ex) => {
403
- try {
404
- const adapter = await getAdapterForExchange(ex);
405
- const [balance, positions, orders] = await Promise.all([
406
- adapter.getBalance(), adapter.getPositions(), adapter.getOpenOrders(),
407
- ]);
408
- // Fetch spot balances for HL and LT (Pacifica is perp-only)
409
- let spotHoldings = [];
410
- let isUnified = false;
411
- try {
412
- if (ex === "hyperliquid") {
413
- const { HyperliquidSpotAdapter } = await import("./exchanges/hyperliquid-spot.js");
414
- const hlSpot = new HyperliquidSpotAdapter(adapter);
415
- await hlSpot.init();
416
- const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
417
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
418
- const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
419
- spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => {
420
- const base = strip(b.token);
421
- return { ...b, valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total) };
422
- });
423
- isUnified = !adapter.dex;
424
- }
425
- else if (ex === "lighter") {
426
- const { LighterAdapter } = await import("./exchanges/lighter.js");
427
- const { LighterSpotAdapter } = await import("./exchanges/lighter-spot.js");
428
- const ltSpot = new LighterSpotAdapter(adapter);
429
- await ltSpot.init();
430
- const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
431
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
432
- spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => ({
433
- ...b,
434
- valueUsd: b.token === "USDC" || b.token === "USDC_SPOT" ? Number(b.total) : (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total),
435
- }));
436
- }
437
- }
438
- catch { /* spot not available */ }
439
- return { exchange: ex, connected: true, balance, positions, openOrders: orders.length, spotHoldings, isUnified, error: undefined };
440
- }
441
- catch (err) {
442
- return { exchange: ex, connected: false, balance: null, positions: [], openOrders: 0, spotHoldings: [], isUnified: false, error: err instanceof Error ? err.message : String(err) };
443
- }
444
- });
445
- const arbPromise = fetchAllFundingRates({ symbols: TOP_SYMBOLS, minSpread: 0 }).catch(() => null);
446
- const [snapshots, arbSnapshot] = await Promise.all([Promise.all(snapshotPromises), arbPromise]);
447
- // Save funding snapshot for sparkline history
448
- if (arbSnapshot) {
449
- try {
450
- const allRates = arbSnapshot.symbols.flatMap(s => s.rates);
451
- if (allRates.length > 0)
452
- saveFundingSnapshot(allRates);
453
- }
454
- catch { /* */ }
455
- }
456
- // Build totals (include non-USDC spot value for unified accounts)
457
- let totalEquity = 0;
458
- let totalSpotValue = 0;
459
- const allPositions = [];
460
- const allSpotHoldings = [];
461
- for (const s of snapshots) {
462
- if (s.balance)
463
- totalEquity += Number(s.balance.equity);
464
- for (const p of s.positions)
465
- allPositions.push({ ...p, exchange: s.exchange });
466
- // For unified accounts (HL), USDC is already in perp equity — only add non-USDC spot
467
- const displaySpot = s.isUnified
468
- ? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
469
- : s.spotHoldings;
470
- const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
471
- totalSpotValue += spotVal;
472
- totalEquity += spotVal;
473
- for (const b of displaySpot)
474
- allSpotHoldings.push({ ...b, exchange: s.exchange });
475
- }
476
- // Top arb
477
- const topArb = arbSnapshot
478
- ? arbSnapshot.symbols.filter(s => s.maxSpreadAnnual >= 5).sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual).slice(0, 5)
479
- : [];
480
- // Risk
481
- const marginUsed = snapshots.reduce((s, x) => s + (x.balance ? Number(x.balance.marginUsed) : 0), 0);
482
- const marginPct = totalEquity > 0 ? (marginUsed / totalEquity) * 100 : 0;
483
- const riskLevel = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
484
- if (json) {
485
- const now = new Date();
486
- const h24 = new Date(now.getTime() - 24 * 3600_000);
487
- return printJson(jsonOk({
488
- version: _pkg.version,
489
- totalEquity,
490
- riskLevel,
491
- exchanges: snapshots.map(s => ({
492
- name: s.exchange, connected: s.connected,
493
- equity: s.balance ? Number(s.balance.equity) : 0,
494
- available: s.balance ? Number(s.balance.available) : 0,
495
- spotHoldings: s.spotHoldings.filter(b => Number(b.total) > 0).map(b => ({
496
- token: b.token, total: b.total, available: b.available, valueUsd: b.valueUsd,
497
- })),
498
- })),
499
- totalSpotValue,
500
- positions: allPositions,
501
- topArbOpportunities: await (async () => {
502
- const { getHistoricalAverages: getAvgs } = await import("./funding-history.js");
503
- const syms = topArb.map(a => a.symbol);
504
- const avgs = syms.length > 0 ? getAvgs(syms, ["hyperliquid", "pacifica", "lighter"]) : new Map();
505
- return topArb.map(a => {
506
- const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
507
- const avg = avgs.get(`${a.symbol}:${bestEx}`);
508
- return {
509
- symbol: a.symbol, spreadAnnual: a.maxSpreadAnnual,
510
- direction: `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`,
511
- avg24h: avg?.avg24h != null ? Math.abs(avg.avg24h) * 8760 * 100 : null,
512
- avg7d: avg?.avg7d != null ? Math.abs(avg.avg7d) * 8760 * 100 : null,
513
- rateHistory: getHistoricalRates(a.symbol, bestEx, h24, now).map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })),
514
- };
515
- });
516
- })(),
517
- }));
518
- }
519
- // ── Terminal UI ──
520
- console.log(chalk.cyan.bold(`\n perp-cli v${_pkg.version}`) + chalk.gray(` — ${snapshots.filter(s => s.connected).length} exchanges connected\n`));
521
- // Balance bars + Top Arb side by side
522
- const now = new Date();
523
- const h24 = new Date(now.getTime() - 24 * 3600_000);
524
- // Left column: Balances (perp + spot per exchange)
525
- // Compute per-exchange totals: perp equity + non-USDC spot (unified) or all spot
526
- const exTotals = snapshots.map(s => {
527
- if (!s.connected || !s.balance)
528
- return { total: 0, margin: 0, equity: 0 };
529
- const equity = Number(s.balance.equity);
530
- const margin = Number(s.balance.marginUsed);
531
- const displaySpot = s.isUnified
532
- ? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
533
- : s.spotHoldings;
534
- const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
535
- return { total: equity + spotVal, margin, equity };
536
- });
537
- const balLines = [];
538
- balLines.push(chalk.white.bold(" Balances"));
539
- for (let i = 0; i < snapshots.length; i++) {
540
- const s = snapshots[i];
541
- if (!s.connected) {
542
- balLines.push(` ${chalk.gray(exAbbr(s.exchange).padEnd(4))} ${chalk.red("disconnected")}`);
543
- continue;
544
- }
545
- const { total, margin, equity } = exTotals[i];
546
- const usagePct = equity > 0 ? (margin / equity) * 100 : 0;
547
- const barFull = Math.min(20, Math.round(usagePct / 5));
548
- const usageColor = usagePct < 30 ? chalk.green : usagePct < 60 ? chalk.yellow : chalk.red;
549
- const bar = usageColor("\u2588".repeat(barFull)) + chalk.gray("\u2591".repeat(20 - barFull));
550
- balLines.push(` ${chalk.white.bold(exAbbr(s.exchange).padEnd(4))} $${formatUsd(total).padEnd(9)} ${bar} ${usagePct.toFixed(0)}% used`);
551
- }
552
- // Spot holdings (non-USDC tokens with value)
553
- const displaySpotHoldings = allSpotHoldings.filter(b => {
554
- const tk = b.token.replace(/[-_]SPOT$/i, "").toUpperCase();
555
- return tk !== "USDC";
556
- });
557
- if (displaySpotHoldings.length > 0) {
558
- balLines.push("");
559
- balLines.push(chalk.white.bold(" Spot Holdings"));
560
- for (const b of displaySpotHoldings) {
561
- const token = b.token.replace(/-SPOT$/i, "");
562
- const ex = exAbbr(b.exchange);
563
- balLines.push(` ${chalk.gray(ex.padEnd(4))} ${chalk.white.bold(token.padEnd(6))} ${b.total.padEnd(12)} ${chalk.gray(`$${formatUsd(b.valueUsd)}`)}`);
564
- }
565
- }
566
- const riskColor = riskLevel === "LOW" ? chalk.green : riskLevel === "MEDIUM" ? chalk.yellow : chalk.red;
567
- balLines.push(` ${"─".repeat(44)}`);
568
- balLines.push(` ${chalk.cyan.bold("Total")} ${chalk.cyan.bold(`$${formatUsd(totalEquity)}`.padEnd(9))} Risk: ${riskColor(riskLevel)}`);
569
- // Right column: Top Arb Opportunities (with 24h/7d averages from local history — 0 API calls)
570
- const { getHistoricalAverages } = await import("./funding-history.js");
571
- const arbSymbols = topArb.map(a => a.symbol);
572
- const arbExchanges = ["hyperliquid", "pacifica", "lighter"];
573
- const histAvgs = arbSymbols.length > 0 ? getHistoricalAverages(arbSymbols, arbExchanges) : new Map();
574
- const arbLines = [];
575
- arbLines.push(chalk.white.bold(" Top Arb Opportunities"));
576
- if (topArb.length > 0) {
577
- // SYMBOL SPREAD 24h TREND 7d DIR
578
- arbLines.push(chalk.gray(" " + "".padEnd(8) + "now".padEnd(9) + "24h".padEnd(7) + "".padEnd(9) + "7d".padEnd(7)));
579
- for (const a of topArb) {
580
- const spreadColor = a.maxSpreadAnnual >= 100 ? chalk.green.bold : a.maxSpreadAnnual >= 30 ? chalk.green : chalk.yellow;
581
- const dir = `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`;
582
- const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
583
- // 24h trend arrow
584
- const history = getHistoricalRates(a.symbol, bestEx, h24, now);
585
- let trend = "";
586
- if (history.length >= 2) {
587
- const oldAnn = Math.abs(history[0].hourlyRate) * 8760 * 100;
588
- const newAnn = Math.abs(history[history.length - 1].hourlyRate) * 8760 * 100;
589
- const delta = newAnn - oldAnn;
590
- const abs = Math.abs(delta);
591
- trend = abs < 1 ? chalk.gray(`\u2500${abs.toFixed(0)}%`)
592
- : delta > 0 ? chalk.red(`\u25B2+${abs.toFixed(0)}%`)
593
- : chalk.green(`\u25BC-${abs.toFixed(0)}%`);
594
- }
595
- // 24h and 7d averages (annualized from hourly)
596
- const avg = histAvgs.get(`${a.symbol}:${bestEx}`);
597
- const avg24h = avg?.avg24h != null ? `${(Math.abs(avg.avg24h) * 8760 * 100).toFixed(0)}%` : "-";
598
- const avg7d = avg?.avg7d != null ? `${(Math.abs(avg.avg7d) * 8760 * 100).toFixed(0)}%` : "-";
599
- arbLines.push(` ${chalk.white.bold(a.symbol.padEnd(8))} ${spreadColor(`${a.maxSpreadAnnual.toFixed(1)}%`.padEnd(9))}${chalk.gray(avg24h.padEnd(7))}${trend.padEnd(9)}${chalk.gray(avg7d.padEnd(7))}${chalk.gray(dir)}`);
600
- }
601
- }
602
- else {
603
- arbLines.push(chalk.gray(" No opportunities above 5%"));
604
- }
605
- // Print layout — side-by-side if terminal is wide enough, else stacked
606
- const stripAnsi = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
607
- const termW = process.stdout.columns || 80;
608
- const SIDE_BY_SIDE_MIN = 100; // 2 indent + 1 + 46 + 1 + 48 + 1
609
- if (termW >= SIDE_BY_SIDE_MIN) {
610
- const LEFT_W = 46;
611
- const RIGHT_W = 48;
612
- const maxLines = Math.max(balLines.length, arbLines.length);
613
- console.log(` ${"┌"}${"─".repeat(LEFT_W)}${"┬"}${"─".repeat(RIGHT_W)}${"┐"}`);
614
- for (let i = 0; i < maxLines; i++) {
615
- const left = balLines[i] ?? "";
616
- const right = arbLines[i] ?? "";
617
- const leftPad = LEFT_W - stripAnsi(left).length;
618
- const rightPad = RIGHT_W - stripAnsi(right).length;
619
- console.log(` \u2502${left}${" ".repeat(Math.max(0, leftPad))}\u2502${right}${" ".repeat(Math.max(0, rightPad))}\u2502`);
620
- }
621
- console.log(` ${"└"}${"─".repeat(LEFT_W)}${"┴"}${"─".repeat(RIGHT_W)}${"┘"}`);
622
- }
623
- else {
624
- // Stacked layout for narrow terminals
625
- const boxW = Math.min(termW - 4, 74); // 2 indent + 2 borders
626
- const printBox = (lines) => {
627
- console.log(` ${"┌"}${"─".repeat(boxW)}${"┐"}`);
628
- for (const line of lines) {
629
- const pad = boxW - stripAnsi(line).length;
630
- console.log(` \u2502${line}${" ".repeat(Math.max(0, pad))}\u2502`);
631
- }
632
- console.log(` ${"└"}${"─".repeat(boxW)}${"┘"}`);
633
- };
634
- printBox(balLines);
635
- console.log();
636
- printBox(arbLines);
637
- }
638
- // Positions
639
- if (allPositions.length > 0) {
640
- console.log(chalk.white.bold("\n Positions"));
641
- const posRows = allPositions.map(p => {
642
- const sideColor = p.side === "long" ? chalk.green : chalk.red;
643
- const notional = Math.abs(Number(p.size) * Number(p.markPrice));
644
- const levStr = p.leverage > 0 ? `${p.leverage}x` : "-";
645
- const warn = p.leverage >= 5 ? chalk.red(" \u26A0") : "";
646
- return [
647
- chalk.white.bold(p.symbol.replace("-PERP", "")),
648
- chalk.gray(exAbbr(p.exchange)),
649
- sideColor(p.side.toUpperCase()),
650
- p.size,
651
- `$${formatUsd(p.entryPrice)}\u2192$${formatUsd(p.markPrice)}`,
652
- formatPnl(p.unrealizedPnl),
653
- `$${formatUsd(notional)}`,
654
- levStr + warn,
655
- ];
656
- });
657
- console.log(makeTable(["Symbol", "Ex", "Side", "Size", "Entry\u2192Mark", "uPnL", "Notional", "Lev"], posRows));
658
- }
659
- else {
660
- console.log(chalk.gray("\n No open positions.\n"));
661
- }
662
- });
663
- });
664
559
  // Switch shared API URLs if --network testnet is used
665
560
  program.hook("preAction", () => {
666
561
  const network = program.opts().network;
@@ -777,8 +672,8 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
777
672
  console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
778
673
  console.log(` ${chalk.green("perp arb scan")} funding arbitrage opportunities`);
779
674
  console.log(` ${chalk.green("perp arb status")} open arb positions + PnL`);
780
- console.log(` ${chalk.green("perp status")} full dashboard`);
781
- console.log(` ${chalk.green("perp dashboard")} live web monitoring`);
675
+ console.log(` ${chalk.green("perp portfolio --arb")} full dashboard (balances + arb top 5)`);
676
+ console.log(` ${chalk.green("perp portfolio --serve")} live web monitoring`);
782
677
  console.log(` ${chalk.green("perp --help")} all commands\n`);
783
678
  }
784
679
  }