perp-cli 0.11.0 → 0.12.1

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 +268 -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
  }
@@ -209,10 +307,40 @@ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToke
209
307
  const { LighterAdapter } = await import("./exchanges/lighter.js");
210
308
  _lighterAdapter = new LighterAdapter("", isTestnet);
211
309
  _lighterAdapter.setSigner(OwsEvmSigner.create(owsWalletName, passphrase));
310
+ // Tier 1: agent if registered (mirrors getAdapter / getAdapterForExchange).
311
+ // Without this, _initWithOws would skip Tier 1 and fall through to
312
+ // auto-setup-at-slot-4 which is wrong for agent users (and breaks under
313
+ // SDK 1.0.11+ where signChangePubKey signature changed).
314
+ const { getAgent: getLtAgentOws } = await import("./agent-wallet/store.js");
315
+ const ltAgentMetaOws = getLtAgentOws("lighter");
316
+ if (ltAgentMetaOws && !noAgent) {
317
+ const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
318
+ const ltL2KeyOws = loadLighterKey(ltAgentMetaOws.accountIndex, ltAgentMetaOws.apiKeyIndex, "");
319
+ _lighterAdapter.setAgentSigner(ltAgentMetaOws, ltL2KeyOws);
320
+ }
321
+ if (noAgent)
322
+ _lighterAdapter.setNoAgent(true);
212
323
  await _lighterAdapter.init();
213
324
  _adapter = _lighterAdapter;
214
325
  return _adapter;
215
326
  }
327
+ case "aster": {
328
+ const { AsterAdapter } = await import("./exchanges/aster.js");
329
+ const asterOws = new AsterAdapter(undefined, isTestnet);
330
+ await asterOws.init();
331
+ asterOws.setMasterSigner(OwsEvmSigner.create(owsWalletName, passphrase));
332
+ // Optionally also set agent if registered
333
+ const { getAgent: getAsterAgentOws } = await import("./agent-wallet/store.js");
334
+ const asterMeta = getAsterAgentOws("aster");
335
+ if (asterMeta) {
336
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
337
+ asterOws.setAgent(asterMeta, agentSigningStrategyFor(asterMeta, owsKeyToken ?? ""));
338
+ }
339
+ if (noAgent)
340
+ asterOws.setNoAgent(true);
341
+ _adapter = asterOws;
342
+ return _adapter;
343
+ }
216
344
  default:
217
345
  throw new Error(`Unknown exchange: ${exchange}`);
218
346
  }
@@ -244,13 +372,11 @@ function getHLAdapter() {
244
372
  registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange);
245
373
  registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
246
374
  registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
247
- registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
375
+ // manage tree wired inside registerWalletCommands below.
248
376
  // stream commands removed
249
377
  registerArbCommands(program, isJson, getAdapterForExchange);
250
- registerWalletCommands(program, isJson);
251
- registerOwsCommands(program, isJson);
252
- registerBridgeCommands(program, isJson);
253
- registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
378
+ registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter);
379
+ registerFundsCommands(program, getAdapter, isJson, () => program.opts().network, getAdapterForExchange);
254
380
  // alert commands removed
255
381
  // Helper to get adapter for a specific exchange (used by arb-auto)
256
382
  async function getAdapterForExchange(rawExchange) {
@@ -258,8 +384,19 @@ async function getAdapterForExchange(rawExchange) {
258
384
  const opts = program.opts();
259
385
  const network = opts.network;
260
386
  const isTestnet = network === "testnet";
261
- const walletName = opts.wallet;
387
+ const owsName = opts.ows;
388
+ const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
389
+ const walletName = owsName ? `ows:${owsName}` : opts.wallet;
390
+ // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
391
+ const noAgent = opts.agent === false;
262
392
  const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
393
+ // Route through OWS init when pk is an OWS reference (e.g. "ows:main").
394
+ // Mirrors getAdapter() at the top of this file. Without this, adapters
395
+ // would try to parse "ows:main" as a raw private key and ethers would
396
+ // throw "invalid BytesLike value" / parseSolanaKeypair would fail.
397
+ if (pk && isOwsKey(pk)) {
398
+ return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
399
+ }
263
400
  switch (exchange) {
264
401
  case "pacifica": {
265
402
  if (_pacificaAdapter)
@@ -269,6 +406,18 @@ async function getAdapterForExchange(rawExchange) {
269
406
  const s1 = loadSettings();
270
407
  const builderCode = process.env.PACIFICA_BUILDER_CODE || s1.referralCodes.pacifica || "PERPCLI";
271
408
  _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
409
+ // Tier 1: agent if registered (Phase 2c)
410
+ const { getAgent: getPacAgentEx } = await import("./agent-wallet/store.js");
411
+ const pacAgentMetaEx = getPacAgentEx("pacifica");
412
+ if (pacAgentMetaEx) {
413
+ const { OwsSolanaSigner: PacOwsSolanaSignerEx } = await import("./signer/ows-solana.js");
414
+ // Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
415
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
416
+ const agentSignerEx = PacOwsSolanaSignerEx.create(pacAgentMetaEx.agentWalletName, "");
417
+ _pacificaAdapter.setAgentSigner(pacAgentMetaEx, agentSignerEx);
418
+ }
419
+ if (noAgent)
420
+ _pacificaAdapter.setNoAgent(true);
272
421
  if (!_adapter)
273
422
  _adapter = _pacificaAdapter;
274
423
  return _pacificaAdapter;
@@ -282,21 +431,35 @@ async function getAdapterForExchange(rawExchange) {
282
431
  await _hlAdapter.init();
283
432
  if (pk) {
284
433
  const s2 = loadSettings();
285
- if (s2.referrals && !s2.referralApplied.hyperliquid) {
434
+ if (!s2.referralApplied.hyperliquid) {
286
435
  const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
287
436
  if (hlRef) {
288
437
  _hlAdapter.autoSetReferrer(hlRef).then(() => {
289
438
  const s = loadSettings();
290
439
  s.referralApplied.hyperliquid = true;
291
440
  saveSettings(s);
292
- }).catch(() => {
293
- const s = loadSettings();
294
- s.referralApplied.hyperliquid = true;
295
- saveSettings(s);
441
+ }).catch((err) => {
442
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
443
+ // Leave referralApplied[ex]=false so next adapter init retries.
444
+ process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
296
445
  });
297
446
  }
298
447
  }
299
448
  }
449
+ // Tier 1: agent if registered
450
+ {
451
+ const { getAgent: getHlAgent2 } = await import("./agent-wallet/store.js");
452
+ const hlAgentMeta2 = getHlAgent2("hyperliquid");
453
+ if (hlAgentMeta2) {
454
+ const { OwsEvmSigner: HlOwsEvmSigner2 } = await import("./signer/ows-evm.js");
455
+ // HL agent wallet was created with empty passphrase (see runHlApproveFlow);
456
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
457
+ const agentSigner2 = HlOwsEvmSigner2.create(hlAgentMeta2.agentWalletName, "");
458
+ _hlAdapter.setAgentSigner(hlAgentMeta2, agentSigner2);
459
+ }
460
+ if (noAgent)
461
+ _hlAdapter.setNoAgent(true);
462
+ }
300
463
  if (!_adapter)
301
464
  _adapter = _hlAdapter;
302
465
  return _hlAdapter;
@@ -305,23 +468,32 @@ async function getAdapterForExchange(rawExchange) {
305
468
  if (_lighterAdapter)
306
469
  return _lighterAdapter;
307
470
  const { LighterAdapter } = await import("./exchanges/lighter.js");
471
+ const { getAgent: getLtAgentEx } = await import("./agent-wallet/store.js");
472
+ const ltAgentMetaEx = getLtAgentEx("lighter");
308
473
  _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
474
+ if (ltAgentMetaEx && !noAgent) {
475
+ // SSOT Rule #3: load agent L2 key from encrypted keystore — no env fallback.
476
+ const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
477
+ const ltL2KeyEx = loadLighterKey(ltAgentMetaEx.accountIndex, ltAgentMetaEx.apiKeyIndex, "");
478
+ _lighterAdapter.setAgentSigner(ltAgentMetaEx, ltL2KeyEx);
479
+ }
480
+ if (noAgent)
481
+ _lighterAdapter.setNoAgent(true);
309
482
  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
- }
483
+ // LT referral apply is L2-signed; trigger on any active tier (agent
484
+ // included), gated by isReadOnly. See first LT case for rationale.
485
+ const s3 = loadSettings();
486
+ if (!s3.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
487
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
488
+ if (ltRef) {
489
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
490
+ const s = loadSettings();
491
+ s.referralApplied.lighter = true;
492
+ saveSettings(s);
493
+ }).catch((err) => {
494
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
495
+ process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
496
+ });
325
497
  }
326
498
  }
327
499
  if (!_adapter)
@@ -330,9 +502,30 @@ async function getAdapterForExchange(rawExchange) {
330
502
  }
331
503
  case "aster": {
332
504
  const { AsterAdapter } = await import("./exchanges/aster.js");
333
- const ast = new AsterAdapter(undefined, undefined, isTestnet);
334
- await ast.init();
335
- return ast;
505
+ const astEx = new AsterAdapter(pk ?? undefined, isTestnet);
506
+ await astEx.init();
507
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet
508
+ const asterExOwsName = opts.ows || _settings.owsActiveWallet;
509
+ if (asterExOwsName) {
510
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
511
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
512
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
513
+ if (pp !== null)
514
+ astEx.setMasterSigner(OwsEvmSigner.create(asterExOwsName, pp));
515
+ }
516
+ // Tier 1: agent if registered
517
+ const owsKeyEx = opts.owsKey || process.env.OWS_API_KEY;
518
+ const { getAgent: getAsterAgentEx } = await import("./agent-wallet/store.js");
519
+ const agentMetaEx = getAsterAgentEx("aster");
520
+ if (agentMetaEx) {
521
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
522
+ astEx.setAgent(agentMetaEx, agentSigningStrategyFor(agentMetaEx, owsKeyEx ?? ""));
523
+ }
524
+ if (noAgent)
525
+ astEx.setNoAgent(true);
526
+ if (!_adapter)
527
+ _adapter = astEx;
528
+ return astEx;
336
529
  }
337
530
  default:
338
531
  throw new Error(`Unknown exchange: ${exchange}`);
@@ -355,312 +548,27 @@ async function getHLAdapterForDex(dex) {
355
548
  }
356
549
  registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
357
550
  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);
551
+ // `agent` is no longer top-level — registered as `wallet agent` inside
552
+ // registerWalletCommands above.
553
+ // withdraw merged into funds.
554
+ // `rebalance` is no longer top-level — registered as `funds rebalance` inside
555
+ // registerFundsCommands above.
556
+ // Background processes & strategies
557
+ import { registerBackgroundCommands } from "./commands/jobs.js";
558
+ registerBackgroundCommands(program, isJson);
559
+ registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
365
560
  // Agent-friendly commands
366
561
  registerRiskCommands(program, getAdapterForExchange, isJson);
367
562
  registerHistoryCommands(program, isJson, getAdapterForExchange);
368
563
  registerSettingsCommands(program, isJson, getAdapterForExchange);
369
564
  // dex commands removed — use 'market hip3' + --dex flag
370
- registerPlanCommands(program, getAdapter, isJson);
565
+ // `plan` is no longer top-level — registered as `strategy plan` inside
566
+ // registerStrategyCommands above.
371
567
  // funding merged into arb — registerFundingCommands removed
372
568
  registerBacktestCommands(program, isJson);
373
- registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
569
+ registerPortfolioCommand(program, getAdapterForExchange, isJson, _pkg.version, getHLAdapterForDex);
374
570
  registerInitCommand(program);
375
571
  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
572
  // Switch shared API URLs if --network testnet is used
665
573
  program.hook("preAction", () => {
666
574
  const network = program.opts().network;
@@ -777,8 +685,8 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
777
685
  console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
778
686
  console.log(` ${chalk.green("perp arb scan")} funding arbitrage opportunities`);
779
687
  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`);
688
+ console.log(` ${chalk.green("perp portfolio --arb")} full dashboard (balances + arb top 5)`);
689
+ console.log(` ${chalk.green("perp portfolio --serve")} live web monitoring`);
782
690
  console.log(` ${chalk.green("perp --help")} all commands\n`);
783
691
  }
784
692
  }