minara 0.3.1 → 0.4.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.
@@ -6,13 +6,297 @@ import { requireAuth } from '../config.js';
6
6
  import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation, validateAddress } from '../utils.js';
7
7
  import { requireTouchId } from '../touchid.js';
8
8
  import { printTxResult, printTable, printKV, POSITION_COLUMNS, FILL_COLUMNS } from '../formatters.js';
9
+ // ─── shared helpers ──────────────────────────────────────────────────────
10
+ const WALLET_OPT = ['-w, --wallet <name>', 'Wallet name or ID'];
11
+ const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
12
+ const pnlFmt = (n) => {
13
+ const color = n >= 0 ? chalk.green : chalk.red;
14
+ return color(`${n >= 0 ? '+' : ''}${fmt(n)}`);
15
+ };
16
+ function getSubAccountId(w) {
17
+ return String(w._id ?? w.id ?? w.subAccountId ?? '');
18
+ }
19
+ function getSubAccountLabel(w) {
20
+ const name = w.name ?? 'Unnamed';
21
+ const def = w.isDefault ? chalk.dim(' (default)') : '';
22
+ return `${name}${def}`;
23
+ }
24
+ /**
25
+ * Normalize the Hyperliquid sub-account summary into a consistent shape.
26
+ * The API returns: { marginSummary: { accountValue, totalNtlPos, totalMarginUsed },
27
+ * withdrawable, assetPositions, ... }
28
+ * But we also handle the flattened shape used in our PerpSubAccount type as fallback.
29
+ */
30
+ function normalizeWalletSummary(raw) {
31
+ const margin = raw.marginSummary;
32
+ if (margin) {
33
+ const rawPositions = Array.isArray(raw.assetPositions)
34
+ ? raw.assetPositions
35
+ : [];
36
+ const positions = rawPositions.map((ap) => {
37
+ const pos = (ap.position && typeof ap.position === 'object'
38
+ ? ap.position : ap);
39
+ return normalizePosition(pos);
40
+ });
41
+ return {
42
+ equity: parseFloat(String(margin.accountValue ?? 0)),
43
+ available: parseFloat(String(raw.withdrawable ?? 0)),
44
+ margin: parseFloat(String(margin.totalMarginUsed ?? 0)),
45
+ unrealizedPnl: parseFloat(String(margin.totalNtlPos ?? 0)),
46
+ positions,
47
+ };
48
+ }
49
+ // Fallback: flattened shape from PerpSubAccount or other responses
50
+ return {
51
+ equity: Number(raw.equityValue ?? raw.accountValue ?? 0),
52
+ available: Number(raw.dispatchableValue ?? raw.withdrawable ?? 0),
53
+ margin: Number(raw.totalMarginUsed ?? 0),
54
+ unrealizedPnl: Number(raw.totalUnrealizedPnl ?? raw.totalNtlPos ?? 0),
55
+ positions: Array.isArray(raw.positions) ? raw.positions : [],
56
+ };
57
+ }
58
+ /**
59
+ * Normalize a Hyperliquid position object to match our POSITION_COLUMNS keys.
60
+ * HL uses: coin, szi (signed size), entryPx, positionValue, unrealizedPnl,
61
+ * leverage: { type, value }, liquidationPx, marginUsed, ...
62
+ */
63
+ function normalizePosition(pos) {
64
+ const szi = parseFloat(String(pos.szi ?? pos.size ?? 0));
65
+ const lev = pos.leverage;
66
+ let leverageVal;
67
+ if (lev && typeof lev === 'object') {
68
+ const lo = lev;
69
+ leverageVal = String(lo.value ?? lo.rawUsd ?? '');
70
+ }
71
+ else if (lev !== undefined && lev !== null) {
72
+ leverageVal = String(lev);
73
+ }
74
+ return {
75
+ symbol: pos.coin ?? pos.symbol ?? '—',
76
+ side: szi > 0 ? 'Long' : szi < 0 ? 'Short' : (pos.side ?? '—'),
77
+ size: Math.abs(szi) || pos.size || '—',
78
+ entryPrice: pos.entryPx ?? pos.entryPrice,
79
+ positionValue: pos.positionValue,
80
+ unrealizedPnl: pos.unrealizedPnl,
81
+ leverage: leverageVal,
82
+ marginUsed: pos.marginUsed,
83
+ liquidationPx: pos.liquidationPx,
84
+ };
85
+ }
86
+ async function fetchSubAccounts(token) {
87
+ const res = await perpsApi.listSubAccounts(token);
88
+ if (!res.success || !res.data)
89
+ return [];
90
+ const raw = res.data;
91
+ if (Array.isArray(raw))
92
+ return raw;
93
+ if (raw && typeof raw === 'object') {
94
+ const inner = raw.data
95
+ ?? raw.subAccounts
96
+ ?? raw.wallets;
97
+ if (Array.isArray(inner))
98
+ return inner;
99
+ }
100
+ return [];
101
+ }
102
+ async function pickSubAccount(token, message = 'Select wallet:') {
103
+ const wallets = await fetchSubAccounts(token);
104
+ if (wallets.length === 0) {
105
+ warn('No perps wallets found.');
106
+ return null;
107
+ }
108
+ if (wallets.length === 1)
109
+ return wallets[0];
110
+ const summaries = await Promise.all(wallets.map((w) => perpsApi.getSubAccountSummary(token, getSubAccountId(w))));
111
+ return select({
112
+ message,
113
+ choices: wallets.map((w, i) => {
114
+ const raw = summaries[i].success && summaries[i].data
115
+ ? summaries[i].data : w;
116
+ const s = normalizeWalletSummary(raw);
117
+ const eq = fmt(s.available);
118
+ const addr = w.address ? chalk.yellow(w.address) : '';
119
+ return {
120
+ name: `${getSubAccountLabel(w)} ${chalk.dim(eq)} ${addr ? chalk.dim(addr.slice(0, 10) + '…') : ''}`,
121
+ value: w,
122
+ };
123
+ }),
124
+ });
125
+ }
126
+ /**
127
+ * Resolve a wallet by name (from --wallet flag) or interactive selection.
128
+ * Returns `{ wallet, walletId }` — walletId is undefined for the default account.
129
+ */
130
+ async function resolveWallet(token, walletName, message = 'Select wallet:') {
131
+ const wallets = await fetchSubAccounts(token);
132
+ if (wallets.length === 0)
133
+ return null;
134
+ let wallet;
135
+ if (walletName) {
136
+ const nameUpper = walletName.toUpperCase();
137
+ const match = wallets.find((w) => (w.name ?? '').toUpperCase() === nameUpper
138
+ || getSubAccountId(w) === walletName);
139
+ if (!match) {
140
+ warn(`Wallet "${walletName}" not found. Available: ${wallets.map((w) => w.name ?? getSubAccountId(w)).join(', ')}`);
141
+ return null;
142
+ }
143
+ wallet = match;
144
+ }
145
+ else if (wallets.length === 1) {
146
+ wallet = wallets[0];
147
+ }
148
+ else {
149
+ const summaries = await Promise.all(wallets.map((w) => perpsApi.getSubAccountSummary(token, getSubAccountId(w))));
150
+ wallet = await select({
151
+ message,
152
+ choices: wallets.map((w, i) => {
153
+ const raw = summaries[i].success && summaries[i].data
154
+ ? summaries[i].data : w;
155
+ const s = normalizeWalletSummary(raw);
156
+ const eq = fmt(s.available);
157
+ return {
158
+ name: `${getSubAccountLabel(w)} ${chalk.dim(eq)}`,
159
+ value: w,
160
+ };
161
+ }),
162
+ });
163
+ }
164
+ const wId = getSubAccountId(wallet);
165
+ return { wallet, walletId: wallet.isDefault ? undefined : (wId || undefined) };
166
+ }
167
+ function parseStrategies(raw) {
168
+ if (Array.isArray(raw))
169
+ return raw;
170
+ if (raw && typeof raw === 'object') {
171
+ const inner = raw.strategies
172
+ ?? raw.data
173
+ ?? raw;
174
+ if (Array.isArray(inner))
175
+ return inner;
176
+ for (const v of Object.values(raw)) {
177
+ if (Array.isArray(v))
178
+ return v;
179
+ }
180
+ }
181
+ return [];
182
+ }
183
+ function extractStrategyName(s) {
184
+ for (const field of ['name', 'strategyName', 'title', 'label', 'displayName']) {
185
+ if (s[field] && typeof s[field] === 'string')
186
+ return String(s[field]);
187
+ }
188
+ // Try config-level name
189
+ if (s.strategyConfig && typeof s.strategyConfig === 'object') {
190
+ const cfg = s.strategyConfig;
191
+ if (cfg.name && typeof cfg.name === 'string')
192
+ return String(cfg.name);
193
+ }
194
+ // Build a descriptive name from symbols + pattern if available
195
+ const symbols = Array.isArray(s.symbols) ? s.symbols : [];
196
+ const symStr = symbols.length > 0
197
+ ? extractSymbolNames(symbols).join('/')
198
+ : undefined;
199
+ if (symStr && s.pattern !== undefined)
200
+ return `${symStr} P${s.pattern}`;
201
+ if (symStr)
202
+ return symStr;
203
+ return undefined;
204
+ }
205
+ function strategyToState(s) {
206
+ const status = String(s.status ?? s.state ?? s.isActive ?? s.enabled ?? '').toLowerCase();
207
+ const isActive = status === 'active' || status === 'enabled' || status === 'running'
208
+ || status === 'true' || s.isActive === true || s.enabled === true;
209
+ const symbols = Array.isArray(s.symbols)
210
+ ? s.symbols.map((sym) => {
211
+ if (typeof sym === 'string')
212
+ return sym;
213
+ if (sym && typeof sym === 'object') {
214
+ const o = sym;
215
+ return String(o.symbol ?? o.name ?? o.coin ?? sym);
216
+ }
217
+ return String(sym);
218
+ })
219
+ : [];
220
+ return {
221
+ active: isActive,
222
+ strategyId: String(s._id ?? s.id ?? s.strategyId ?? ''),
223
+ name: extractStrategyName(s),
224
+ symbols,
225
+ subAccountId: s.subAccountId ? String(s.subAccountId) : undefined,
226
+ strategyConfig: s.strategyConfig && typeof s.strategyConfig === 'object'
227
+ ? s.strategyConfig : undefined,
228
+ language: s.language ? String(s.language) : undefined,
229
+ createdAt: s.createdAt ? String(s.createdAt) : undefined,
230
+ updatedAt: s.updatedAt ? String(s.updatedAt) : undefined,
231
+ raw: s,
232
+ };
233
+ }
234
+ async function getAutopilotState(token) {
235
+ const res = await perpsApi.getStrategies(token);
236
+ if (!res.success || !res.data)
237
+ return { active: false };
238
+ const strategies = parseStrategies(res.data);
239
+ if (strategies.length === 0)
240
+ return { active: false };
241
+ return strategyToState(strategies[0]);
242
+ }
243
+ async function getAllAutopilotStates(token) {
244
+ const res = await perpsApi.getStrategies(token);
245
+ if (!res.success || !res.data)
246
+ return [];
247
+ return parseStrategies(res.data).map(strategyToState);
248
+ }
249
+ function getAutopilotForSubAccount(states, subAccountId) {
250
+ return states.find((s) => s.subAccountId === subAccountId);
251
+ }
252
+ function getAllStrategiesForWallet(states, walletId, isDefault) {
253
+ return states.filter((s) => {
254
+ if (s.subAccountId === walletId)
255
+ return true;
256
+ if (isDefault && !s.subAccountId)
257
+ return true;
258
+ return false;
259
+ });
260
+ }
261
+ function strategyDisplayName(s) {
262
+ if (s.name)
263
+ return s.name;
264
+ if (s.strategyId)
265
+ return s.strategyId.length > 12 ? s.strategyId.slice(0, 12) + '…' : s.strategyId;
266
+ return 'Unnamed';
267
+ }
268
+ /** Normalize supported-symbols response: handles string[], object[] with symbol/name key, or nested structures. */
269
+ function extractSymbolNames(data) {
270
+ const fallback = ['BTC', 'ETH', 'SOL'];
271
+ if (!data)
272
+ return fallback;
273
+ const arr = Array.isArray(data) ? data : [];
274
+ if (arr.length === 0)
275
+ return fallback;
276
+ return arr.map((item) => {
277
+ if (typeof item === 'string')
278
+ return item;
279
+ if (item && typeof item === 'object') {
280
+ const obj = item;
281
+ const name = obj.symbol ?? obj.name ?? obj.coin ?? obj.asset ?? obj.ticker;
282
+ if (typeof name === 'string')
283
+ return name;
284
+ }
285
+ return String(item);
286
+ });
287
+ }
9
288
  // ─── deposit ─────────────────────────────────────────────────────────────
10
289
  const depositCmd = new Command('deposit')
11
290
  .description('Deposit USDC into Hyperliquid perps (min 5 USDC)')
12
291
  .option('-a, --amount <amount>', 'USDC amount')
292
+ .option(WALLET_OPT[0], WALLET_OPT[1])
13
293
  .option('-y, --yes', 'Skip confirmation')
14
294
  .action(wrapAction(async (opts) => {
15
295
  const creds = requireAuth();
296
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Deposit to which wallet?');
297
+ if (!resolved)
298
+ return;
299
+ const { wallet, walletId } = resolved;
16
300
  const amount = opts.amount
17
301
  ? parseFloat(opts.amount)
18
302
  : await numberPrompt({ message: 'USDC amount to deposit (min 5):', min: 5, required: true });
@@ -20,19 +304,19 @@ const depositCmd = new Command('deposit')
20
304
  console.error(chalk.red('✖'), 'Minimum deposit is 5 USDC');
21
305
  process.exit(1);
22
306
  }
23
- console.log(`\n Deposit : ${chalk.bold(amount)} USDC → Perps\n`);
307
+ console.log(`\n Deposit : ${chalk.bold(amount)} USDC → ${getSubAccountLabel(wallet)}\n`);
24
308
  if (!opts.yes) {
25
309
  const ok = await confirm({ message: 'Confirm deposit?', default: true });
26
310
  if (!ok)
27
311
  return;
28
312
  }
29
- await requireTransactionConfirmation(`Deposit ${amount} USDC → Perps`);
313
+ await requireTransactionConfirmation(`Deposit ${amount} USDC → ${wallet.name ?? 'Perps'}`);
30
314
  await requireTouchId();
31
315
  const spin = spinner('Depositing…');
32
- const res = await perpsApi.deposit(creds.accessToken, { usdcAmount: amount });
316
+ const res = await perpsApi.deposit(creds.accessToken, { usdcAmount: amount, subAccountId: walletId });
33
317
  spin.stop();
34
318
  assertApiOk(res, 'Deposit failed');
35
- success(`Deposited ${amount} USDC`);
319
+ success(`Deposited ${amount} USDC to ${getSubAccountLabel(wallet)}`);
36
320
  printTxResult(res.data);
37
321
  }));
38
322
  // ─── withdraw ────────────────────────────────────────────────────────────
@@ -40,9 +324,14 @@ const withdrawCmd = new Command('withdraw')
40
324
  .description('Withdraw USDC from Hyperliquid perps')
41
325
  .option('-a, --amount <amount>', 'USDC amount')
42
326
  .option('--to <address>', 'Destination address')
327
+ .option(WALLET_OPT[0], WALLET_OPT[1])
43
328
  .option('-y, --yes', 'Skip confirmation')
44
329
  .action(wrapAction(async (opts) => {
45
330
  const creds = requireAuth();
331
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Withdraw from which wallet?');
332
+ if (!resolved)
333
+ return;
334
+ const { wallet, walletId } = resolved;
46
335
  const amount = opts.amount
47
336
  ? parseFloat(opts.amount)
48
337
  : await numberPrompt({ message: 'USDC amount to withdraw:', min: 0.01, required: true });
@@ -50,7 +339,7 @@ const withdrawCmd = new Command('withdraw')
50
339
  message: 'Destination address:',
51
340
  validate: (v) => validateAddress(v, 'arbitrum'),
52
341
  });
53
- console.log(`\n Withdraw : ${chalk.bold(amount)} USDC → ${chalk.yellow(toAddress)}\n`);
342
+ console.log(`\n Withdraw : ${chalk.bold(amount)} USDC from ${getSubAccountLabel(wallet)} → ${chalk.yellow(toAddress)}\n`);
54
343
  warn('Withdrawals may take time to process.');
55
344
  if (!opts.yes) {
56
345
  const ok = await confirm({ message: 'Confirm withdrawal?', default: false });
@@ -60,7 +349,7 @@ const withdrawCmd = new Command('withdraw')
60
349
  await requireTransactionConfirmation(`Withdraw ${amount} USDC → ${toAddress}`);
61
350
  await requireTouchId();
62
351
  const spin = spinner('Withdrawing…');
63
- const res = await perpsApi.withdraw(creds.accessToken, { usdcAmount: amount, toAddress });
352
+ const res = await perpsApi.withdraw(creds.accessToken, { usdcAmount: amount, toAddress, subAccountId: walletId });
64
353
  spin.stop();
65
354
  assertApiOk(res, 'Withdrawal failed');
66
355
  success('Withdrawal submitted');
@@ -69,53 +358,125 @@ const withdrawCmd = new Command('withdraw')
69
358
  // ─── positions ───────────────────────────────────────────────────────────
70
359
  const positionsCmd = new Command('positions')
71
360
  .alias('pos')
72
- .description('View all open perps positions')
73
- .action(wrapAction(async () => {
361
+ .description('View open perps positions')
362
+ .option(WALLET_OPT[0], WALLET_OPT[1])
363
+ .action(wrapAction(async (opts) => {
74
364
  const creds = requireAuth();
75
- const spin = spinner('Fetching positions…');
76
- const res = await perpsApi.getAccountSummary(creds.accessToken);
365
+ // If --wallet specified, show only that wallet
366
+ if (opts.wallet) {
367
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'View positions for which wallet?');
368
+ if (!resolved)
369
+ return;
370
+ const { wallet, walletId } = resolved;
371
+ const wId = walletId ?? getSubAccountId(wallet);
372
+ const spin = spinner(`Fetching ${wallet.name ?? 'wallet'}…`);
373
+ const sumRes = await perpsApi.getSubAccountSummary(creds.accessToken, wId);
374
+ spin.stop();
375
+ const raw = sumRes.success && sumRes.data ? sumRes.data : wallet;
376
+ const s = normalizeWalletSummary(raw);
377
+ console.log('');
378
+ console.log(chalk.bold(`${getSubAccountLabel(wallet)}:`));
379
+ console.log(` Equity : ${fmt(s.equity)}`);
380
+ console.log(` Unrealized PnL: ${pnlFmt(s.unrealizedPnl)}`);
381
+ console.log(` Margin Used : ${fmt(s.margin)}`);
382
+ console.log('');
383
+ console.log(chalk.bold(`Open Positions (${s.positions.length}):`));
384
+ if (s.positions.length === 0) {
385
+ console.log(chalk.dim(' No open positions.'));
386
+ }
387
+ else {
388
+ printTable(s.positions, POSITION_COLUMNS);
389
+ }
390
+ console.log('');
391
+ return;
392
+ }
393
+ // No --wallet: show all wallets
394
+ const spin = spinner('Fetching wallets…');
395
+ const wallets = await fetchSubAccounts(creds.accessToken);
77
396
  spin.stop();
78
- if (!res.success || !res.data) {
79
- console.log(chalk.dim('Could not fetch positions.'));
80
- if (res.error?.message)
81
- console.log(chalk.dim(` ${res.error.message}`));
397
+ if (wallets.length === 0) {
398
+ // Fallback to legacy single-wallet API
399
+ const legSpin = spinner('Fetching positions…');
400
+ const res = await perpsApi.getAccountSummary(creds.accessToken);
401
+ legSpin.stop();
402
+ if (!res.success || !res.data) {
403
+ console.log(chalk.dim('Could not fetch positions.'));
404
+ return;
405
+ }
406
+ const s = normalizeWalletSummary(res.data);
407
+ console.log('');
408
+ console.log(` Equity : ${fmt(s.equity)}`);
409
+ console.log(` Unrealized PnL: ${pnlFmt(s.unrealizedPnl)}`);
410
+ console.log(` Margin Used : ${fmt(s.margin)}`);
411
+ console.log('');
412
+ console.log(chalk.bold(`Open Positions (${s.positions.length}):`));
413
+ if (s.positions.length === 0) {
414
+ console.log(chalk.dim(' No open positions.'));
415
+ }
416
+ else {
417
+ printTable(s.positions, POSITION_COLUMNS);
418
+ }
419
+ console.log('');
82
420
  return;
83
421
  }
84
- const d = res.data;
85
- const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
86
- const pnlFmt = (n) => {
87
- const color = n >= 0 ? chalk.green : chalk.red;
88
- return color(`${n >= 0 ? '+' : ''}${fmt(n)}`);
89
- };
90
- console.log('');
91
- console.log(` Equity : ${fmt(Number(d.equityValue ?? 0))}`);
92
- console.log(` Unrealized PnL: ${pnlFmt(Number(d.totalUnrealizedPnl ?? 0))}`);
93
- console.log(` Margin Used : ${fmt(Number(d.totalMarginUsed ?? 0))}`);
94
- const positions = Array.isArray(d.positions) ? d.positions : [];
95
- console.log('');
96
- console.log(chalk.bold(`Open Positions (${positions.length}):`));
97
- if (positions.length === 0) {
98
- console.log(chalk.dim(' No open positions.'));
422
+ // Multi-wallet: fetch all summaries in parallel
423
+ const sumSpin = spinner('Fetching wallet summaries…');
424
+ const summaryResults = await Promise.all(wallets.map((w) => perpsApi.getSubAccountSummary(creds.accessToken, getSubAccountId(w))));
425
+ const aggRes = await perpsApi.getAggregatedSummary(creds.accessToken);
426
+ sumSpin.stop();
427
+ let totalPositions = 0;
428
+ for (let i = 0; i < wallets.length; i++) {
429
+ const w = wallets[i];
430
+ const sumRes = summaryResults[i];
431
+ const raw = sumRes.success && sumRes.data ? sumRes.data : w;
432
+ const s = normalizeWalletSummary(raw);
433
+ totalPositions += s.positions.length;
434
+ console.log('');
435
+ console.log(chalk.bold(`${getSubAccountLabel(w)}:`));
436
+ console.log(` Equity : ${fmt(s.equity)}`);
437
+ console.log(` Unrealized PnL: ${pnlFmt(s.unrealizedPnl)}`);
438
+ console.log(` Margin Used : ${fmt(s.margin)}`);
439
+ if (s.positions.length === 0) {
440
+ console.log(chalk.dim(' No open positions.'));
441
+ }
442
+ else {
443
+ printTable(s.positions, POSITION_COLUMNS);
444
+ }
99
445
  }
100
- else {
101
- printTable(positions, POSITION_COLUMNS);
446
+ console.log('');
447
+ if (aggRes.success && aggRes.data) {
448
+ const agg = aggRes.data;
449
+ const aggS = normalizeWalletSummary(agg);
450
+ console.log(chalk.bold('Aggregated:'));
451
+ console.log(` Total Equity : ${fmt(aggS.equity || Number(agg.totalEquity ?? 0))}`);
452
+ console.log(` Total Unrl. PnL : ${pnlFmt(aggS.unrealizedPnl || Number(agg.totalUnrealizedPnl ?? 0))}`);
453
+ console.log(` Total Margin : ${fmt(aggS.margin || Number(agg.totalMarginUsed ?? 0))}`);
102
454
  }
455
+ console.log(chalk.dim(` Total positions: ${totalPositions}`));
103
456
  console.log('');
104
457
  }));
105
458
  // ─── order ───────────────────────────────────────────────────────────────
106
459
  const orderCmd = new Command('order')
107
460
  .description('Place a perps order')
461
+ .option(WALLET_OPT[0], WALLET_OPT[1])
108
462
  .option('-y, --yes', 'Skip confirmation')
109
463
  .action(wrapAction(async (opts) => {
110
464
  const creds = requireAuth();
111
- // Check autopilot block manual orders while AI is trading
465
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Place order on which wallet?');
466
+ if (!resolved)
467
+ return;
468
+ const { wallet, walletId } = resolved;
469
+ // Check autopilot for this wallet
112
470
  const apSpin = spinner('Checking autopilot…');
113
- const apState = await getAutopilotState(creds.accessToken);
471
+ const allStates = await getAllAutopilotStates(creds.accessToken);
114
472
  apSpin.stop();
115
- if (apState.active) {
473
+ const wId = getSubAccountId(wallet);
474
+ const walletStrategies = getAllStrategiesForWallet(allStates, wId, !!wallet.isDefault);
475
+ const activeStrategy = walletStrategies.find((s) => s.active);
476
+ if (activeStrategy) {
116
477
  console.log('');
117
- warn('Autopilot is currently ON. Manual order placement is disabled while AI is trading.');
118
- info(`Trading symbols: ${apState.symbols?.join(', ') ?? 'unknown'}`);
478
+ warn(`Autopilot "${strategyDisplayName(activeStrategy)}" is ON for "${wallet.name ?? 'this wallet'}". Manual order placement is disabled while AI is trading.`);
479
+ info(`Trading symbols: ${activeStrategy.symbols?.join(', ') ?? 'unknown'}`);
119
480
  info('Turn off autopilot first: minara perps autopilot');
120
481
  console.log('');
121
482
  return;
@@ -228,38 +589,51 @@ const orderCmd = new Command('order')
228
589
  }
229
590
  await requireTouchId();
230
591
  const spin = spinner('Placing order…');
231
- const res = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping });
592
+ const res = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping, subAccountId: walletId });
232
593
  spin.stop();
233
594
  assertApiOk(res, 'Order placement failed');
234
- success('Order submitted!');
595
+ success(`Order submitted on ${getSubAccountLabel(wallet)}!`);
235
596
  printTxResult(res.data);
236
597
  }));
237
598
  // ─── cancel ──────────────────────────────────────────────────────────────
238
599
  const cancelCmd = new Command('cancel')
239
600
  .description('Cancel perps orders')
601
+ .option(WALLET_OPT[0], WALLET_OPT[1])
240
602
  .option('-y, --yes', 'Skip confirmation')
241
603
  .action(wrapAction(async (opts) => {
242
604
  const creds = requireAuth();
243
- const spin = spinner('Fetching open orders…');
244
- const address = await perpsApi.getPerpsAddress(creds.accessToken);
245
- if (!address) {
246
- spin.stop();
247
- warn('Could not find your perps wallet address. Make sure your perps account is initialized.');
605
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Cancel orders on which wallet?');
606
+ if (!resolved)
248
607
  return;
608
+ const { wallet, walletId } = resolved;
609
+ const spin = spinner('Fetching open orders…');
610
+ const wId = getSubAccountId(wallet);
611
+ let openOrders;
612
+ if (walletId) {
613
+ const ordRes = await perpsApi.getSubAccountOpenOrders(creds.accessToken, wId);
614
+ openOrders = ordRes.success && Array.isArray(ordRes.data) ? ordRes.data : [];
615
+ }
616
+ else {
617
+ const address = await perpsApi.getPerpsAddress(creds.accessToken);
618
+ if (!address) {
619
+ spin.stop();
620
+ warn('Could not find your perps wallet address.');
621
+ return;
622
+ }
623
+ openOrders = await perpsApi.getOpenOrders(address);
249
624
  }
250
- const openOrders = await perpsApi.getOpenOrders(address);
251
625
  spin.stop();
252
626
  if (openOrders.length === 0) {
253
- info('No open orders to cancel.');
627
+ info(`No open orders on ${getSubAccountLabel(wallet)}.`);
254
628
  return;
255
629
  }
256
630
  const selected = await select({
257
631
  message: 'Select order to cancel:',
258
632
  choices: openOrders.map((o) => {
259
633
  const side = o.side === 'B' ? chalk.green('BUY') : chalk.red('SELL');
260
- const px = `$${Number(o.limitPx).toLocaleString()}`;
634
+ const px = `$${Number(o.limitPx ?? 0).toLocaleString()}`;
261
635
  return {
262
- name: `${chalk.bold(o.coin.padEnd(6))} ${side} ${o.sz} @ ${chalk.yellow(px)} ${chalk.dim(`oid:${o.oid}`)}`,
636
+ name: `${chalk.bold(String(o.coin ?? '').padEnd(6))} ${side} ${o.sz} @ ${chalk.yellow(px)} ${chalk.dim(`oid:${o.oid}`)}`,
263
637
  value: o,
264
638
  };
265
639
  }),
@@ -267,7 +641,7 @@ const cancelCmd = new Command('cancel')
267
641
  if (!opts.yes) {
268
642
  const sideLabel = selected.side === 'B' ? 'BUY' : 'SELL';
269
643
  const ok = await confirm({
270
- message: `Cancel ${sideLabel} ${selected.coin} ${selected.sz} @ $${Number(selected.limitPx).toLocaleString()}?`,
644
+ message: `Cancel ${sideLabel} ${selected.coin} ${selected.sz} @ $${Number(selected.limitPx ?? 0).toLocaleString()}?`,
271
645
  default: false,
272
646
  });
273
647
  if (!ok)
@@ -275,7 +649,8 @@ const cancelCmd = new Command('cancel')
275
649
  }
276
650
  const cancelSpin = spinner('Cancelling…');
277
651
  const res = await perpsApi.cancelOrders(creds.accessToken, {
278
- cancels: [{ a: selected.coin, o: selected.oid }],
652
+ cancels: [{ a: String(selected.coin), o: Number(selected.oid) }],
653
+ subAccountId: walletId,
279
654
  });
280
655
  cancelSpin.stop();
281
656
  assertApiOk(res, 'Order cancellation failed');
@@ -285,23 +660,32 @@ const cancelCmd = new Command('cancel')
285
660
  // ─── close position ─────────────────────────────────────────────────────
286
661
  const closeCmd = new Command('close')
287
662
  .description('Close an open perps position at market price')
663
+ .option(WALLET_OPT[0], WALLET_OPT[1])
288
664
  .option('-y, --yes', 'Skip confirmation')
289
665
  .option('-a, --all', 'Close all open positions (non-interactive)')
290
666
  .option('-s, --symbol <symbol>', 'Close position by symbol (non-interactive, e.g. BTC, ETH)')
291
667
  .action(wrapAction(async (opts) => {
292
668
  const creds = requireAuth();
669
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Close position on which wallet?');
670
+ if (!resolved)
671
+ return;
672
+ const { wallet, walletId } = resolved;
293
673
  const spin = spinner('Fetching positions…');
294
- const res = await perpsApi.getAccountSummary(creds.accessToken);
674
+ let d;
675
+ const wId = getSubAccountId(wallet);
676
+ if (walletId) {
677
+ const sumRes = await perpsApi.getSubAccountSummary(creds.accessToken, wId);
678
+ d = sumRes.success && sumRes.data ? sumRes.data : {};
679
+ }
680
+ else {
681
+ const res = await perpsApi.getAccountSummary(creds.accessToken);
682
+ d = res.success && res.data ? res.data : {};
683
+ }
295
684
  const assets = await perpsApi.getAssetMeta();
296
685
  spin.stop();
297
- if (!res.success || !res.data) {
298
- warn('Could not fetch positions.');
299
- return;
300
- }
301
- const d = res.data;
302
686
  const positions = Array.isArray(d.positions) ? d.positions : [];
303
687
  if (positions.length === 0) {
304
- info('No open positions to close.');
688
+ info(`No open positions on ${getSubAccountLabel(wallet)}.`);
305
689
  return;
306
690
  }
307
691
  const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
@@ -351,7 +735,7 @@ const closeCmd = new Command('close')
351
735
  t: { trigger: { triggerPx: String(marketPx), tpsl: 'tp', isMarket: true } },
352
736
  };
353
737
  try {
354
- const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na' });
738
+ const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na', subAccountId: walletId });
355
739
  if (orderRes.success) {
356
740
  results.push({ symbol, side, success: true });
357
741
  }
@@ -455,7 +839,7 @@ const closeCmd = new Command('close')
455
839
  }
456
840
  await requireTouchId();
457
841
  const orderSpin = spinner('Closing position…');
458
- const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na' });
842
+ const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na', subAccountId: walletId });
459
843
  orderSpin.stop();
460
844
  assertApiOk(orderRes, 'Close position failed');
461
845
  success(`Position closed — ${sideLabel} ${symbol} ${sz}`);
@@ -464,8 +848,13 @@ const closeCmd = new Command('close')
464
848
  // ─── leverage ────────────────────────────────────────────────────────────
465
849
  const leverageCmd = new Command('leverage')
466
850
  .description('Update leverage for a symbol')
467
- .action(wrapAction(async () => {
851
+ .option(WALLET_OPT[0], WALLET_OPT[1])
852
+ .action(wrapAction(async (opts) => {
468
853
  const creds = requireAuth();
854
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Update leverage on which wallet?');
855
+ if (!resolved)
856
+ return;
857
+ const { wallet, walletId } = resolved;
469
858
  const metaSpin = spinner('Fetching available assets…');
470
859
  const assets = await perpsApi.getAssetMeta();
471
860
  metaSpin.stop();
@@ -501,27 +890,40 @@ const leverageCmd = new Command('leverage')
501
890
  ],
502
891
  });
503
892
  const spin = spinner('Updating leverage…');
504
- const res = await perpsApi.updateLeverage(creds.accessToken, { symbol, isCross, leverage: leverage });
893
+ const res = await perpsApi.updateLeverage(creds.accessToken, { symbol, isCross, leverage: leverage, subAccountId: walletId });
505
894
  spin.stop();
506
895
  assertApiOk(res, 'Failed to update leverage');
507
- success(`Leverage set to ${leverage}x (${isCross ? 'cross' : 'isolated'}) for ${symbol}`);
896
+ success(`Leverage set to ${leverage}x (${isCross ? 'cross' : 'isolated'}) for ${symbol} on ${getSubAccountLabel(wallet)}`);
508
897
  }));
509
898
  // ─── trades ──────────────────────────────────────────────────────────────
510
899
  const tradesCmd = new Command('trades')
511
900
  .description('View your perps trade fills')
512
901
  .option('-n, --count <n>', 'Number of recent fills to show', '20')
513
902
  .option('-d, --days <n>', 'Look back N days', '7')
903
+ .option(WALLET_OPT[0], WALLET_OPT[1])
514
904
  .action(wrapAction(async (opts) => {
515
905
  const creds = requireAuth();
516
- const spin = spinner('Fetching trade history…');
517
- const address = await perpsApi.getPerpsAddress(creds.accessToken);
518
- if (!address) {
519
- spin.stop();
520
- warn('Could not find your perps wallet address. Make sure your perps account is initialized.');
906
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'View trades for which wallet?');
907
+ if (!resolved)
521
908
  return;
522
- }
909
+ const { wallet, walletId } = resolved;
523
910
  const days = Math.max(1, parseInt(opts.days, 10) || 7);
524
- const fills = await perpsApi.getUserFills(address, days);
911
+ const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
912
+ const spin = spinner('Fetching trade history…');
913
+ let fills;
914
+ if (walletId) {
915
+ const fillRes = await perpsApi.getSubAccountFills(creds.accessToken, walletId, startTime);
916
+ fills = fillRes.success && Array.isArray(fillRes.data) ? fillRes.data : [];
917
+ }
918
+ else {
919
+ const address = await perpsApi.getPerpsAddress(creds.accessToken);
920
+ if (!address) {
921
+ spin.stop();
922
+ warn('Could not find your perps wallet address.');
923
+ return;
924
+ }
925
+ fills = await perpsApi.getUserFills(address, days);
926
+ }
525
927
  spin.stop();
526
928
  const limit = Math.max(1, parseInt(opts.count, 10) || 20);
527
929
  const recent = fills.slice(0, limit);
@@ -530,11 +932,11 @@ const tradesCmd = new Command('trades')
530
932
  const closingFills = fills.filter((f) => Number(f.closedPnl ?? 0) !== 0);
531
933
  const wins = closingFills.filter((f) => Number(f.closedPnl) > 0).length;
532
934
  const pnlColor = totalPnl >= 0 ? chalk.green : chalk.red;
533
- const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
935
+ const fmtLocal = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
534
936
  console.log('');
535
- console.log(chalk.bold(`Trade Fills (last ${days}d — ${fills.length} fills):`));
536
- console.log(` Realized PnL : ${pnlColor(`${totalPnl >= 0 ? '+' : ''}${fmt(totalPnl)}`)}`);
537
- console.log(` Total Fees : ${chalk.dim(fmt(totalFees))}`);
937
+ console.log(chalk.bold(`Trade Fills — ${getSubAccountLabel(wallet)} (last ${days}d — ${fills.length} fills):`));
938
+ console.log(` Realized PnL : ${pnlColor(`${totalPnl >= 0 ? '+' : ''}${fmtLocal(totalPnl)}`)}`);
939
+ console.log(` Total Fees : ${chalk.dim(fmtLocal(totalFees))}`);
538
940
  if (closingFills.length > 0) {
539
941
  console.log(` Win Rate : ${wins}/${closingFills.length} (${((wins / closingFills.length) * 100).toFixed(1)}%)`);
540
942
  }
@@ -553,166 +955,788 @@ const fundRecordsCmd = new Command('fund-records')
553
955
  .description('View perps fund deposit/withdraw records')
554
956
  .option('-p, --page <n>', 'Page', '1')
555
957
  .option('-l, --limit <n>', 'Limit', '20')
958
+ .option(WALLET_OPT[0], WALLET_OPT[1])
556
959
  .action(wrapAction(async (opts) => {
557
960
  const creds = requireAuth();
961
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'View records for which wallet?');
962
+ if (!resolved)
963
+ return;
964
+ const { wallet, walletId } = resolved;
965
+ const page = parseInt(opts.page, 10);
966
+ const limit = parseInt(opts.limit, 10);
558
967
  const spin = spinner('Fetching records…');
559
- const res = await perpsApi.getFundRecords(creds.accessToken, parseInt(opts.page, 10), parseInt(opts.limit, 10));
968
+ let data;
969
+ if (walletId) {
970
+ const wId = getSubAccountId(wallet);
971
+ const recRes = await perpsApi.getSubAccountRecords(creds.accessToken, wId, page, limit);
972
+ data = recRes.success && Array.isArray(recRes.data) ? recRes.data : [];
973
+ }
974
+ else {
975
+ const res = await perpsApi.getFundRecords(creds.accessToken, page, limit);
976
+ assertApiOk(res, 'Failed to fetch fund records');
977
+ data = Array.isArray(res.data) ? res.data : [];
978
+ }
560
979
  spin.stop();
561
- assertApiOk(res, 'Failed to fetch fund records');
562
980
  console.log('');
563
- console.log(chalk.bold('Fund Records:'));
564
- if (Array.isArray(res.data) && res.data.length > 0) {
565
- printTable(res.data);
981
+ console.log(chalk.bold(`Fund Records — ${getSubAccountLabel(wallet)}:`));
982
+ if (data && data.length > 0) {
983
+ printTable(data);
566
984
  }
567
985
  else {
568
986
  console.log(chalk.dim(' No fund records.'));
569
987
  }
570
988
  console.log('');
571
989
  }));
572
- async function getAutopilotState(token) {
573
- const res = await perpsApi.getStrategies(token);
574
- if (!res.success || !res.data)
575
- return { active: false };
576
- // Response may be an array or { data: [...] } or nested object
577
- let strategies = [];
578
- const raw = res.data;
579
- if (Array.isArray(raw)) {
580
- strategies = raw;
990
+ // ─── autopilot ──────────────────────────────────────────────────────────
991
+ const autopilotCmd = new Command('autopilot')
992
+ .alias('ap')
993
+ .description('Manage AI autopilot trading strategy (per wallet)')
994
+ .option(WALLET_OPT[0], WALLET_OPT[1])
995
+ .action(wrapAction(async (opts) => {
996
+ const creds = requireAuth();
997
+ const loadSpin = spinner('Loading wallets & strategies…');
998
+ const [wallets, allStates, supported] = await Promise.all([
999
+ fetchSubAccounts(creds.accessToken),
1000
+ getAllAutopilotStates(creds.accessToken),
1001
+ perpsApi.getSupportedSymbols(creds.accessToken).then((r) => extractSymbolNames(r.success ? r.data : null)),
1002
+ ]);
1003
+ loadSpin.stop();
1004
+ // ── Pick wallet ──────────────────────────────────────────────────
1005
+ let wallet;
1006
+ if (wallets.length === 0) {
1007
+ warn('No perps wallets found.');
1008
+ return;
581
1009
  }
582
- else if (raw && typeof raw === 'object') {
583
- // Might be wrapped in { strategies: [...] } or { data: [...] }
584
- const inner = raw.strategies
585
- ?? raw.data
586
- ?? raw;
587
- if (Array.isArray(inner)) {
588
- strategies = inner;
1010
+ else if (opts.wallet) {
1011
+ const nameUpper = opts.wallet.toUpperCase();
1012
+ const match = wallets.find((w) => (w.name ?? '').toUpperCase() === nameUpper || getSubAccountId(w) === opts.wallet);
1013
+ if (!match) {
1014
+ warn(`Wallet "${opts.wallet}" not found. Available: ${wallets.map((w) => w.name ?? getSubAccountId(w)).join(', ')}`);
1015
+ return;
589
1016
  }
590
- else {
591
- for (const v of Object.values(raw)) {
592
- if (Array.isArray(v)) {
593
- strategies.push(...v);
1017
+ wallet = match;
1018
+ }
1019
+ else if (wallets.length === 1) {
1020
+ wallet = wallets[0];
1021
+ }
1022
+ else {
1023
+ const summaries = await Promise.all(wallets.map((w) => perpsApi.getSubAccountSummary(creds.accessToken, getSubAccountId(w))));
1024
+ wallet = await select({
1025
+ message: 'Select wallet for autopilot:',
1026
+ choices: wallets.map((w, i) => {
1027
+ const wId = getSubAccountId(w);
1028
+ const wStrategies = getAllStrategiesForWallet(allStates, wId, !!w.isDefault);
1029
+ const activeCount = wStrategies.filter((st) => st.active).length;
1030
+ let apLabel;
1031
+ if (wStrategies.length === 0) {
1032
+ apLabel = chalk.dim(' [No Strategy]');
1033
+ }
1034
+ else if (activeCount > 0) {
1035
+ apLabel = chalk.green(` [${activeCount}/${wStrategies.length} ON]`);
1036
+ }
1037
+ else {
1038
+ apLabel = chalk.dim(` [${wStrategies.length} strategies, all OFF]`);
1039
+ }
1040
+ const raw = summaries[i].success && summaries[i].data
1041
+ ? summaries[i].data : w;
1042
+ const s = normalizeWalletSummary(raw);
1043
+ return {
1044
+ name: `${getSubAccountLabel(w)} ${chalk.dim(fmt(s.available))}${apLabel}`,
1045
+ value: w,
1046
+ };
1047
+ }),
1048
+ });
1049
+ }
1050
+ const walletId = getSubAccountId(wallet);
1051
+ const walletStrategies = getAllStrategiesForWallet(allStates, walletId, !!wallet.isDefault);
1052
+ // ── No strategies for this wallet — offer create or attach ───────
1053
+ if (walletStrategies.length === 0) {
1054
+ console.log('');
1055
+ info(`No autopilot strategies on ${getSubAccountLabel(wallet)}.`);
1056
+ const unbound = allStates.filter((s) => s.strategyId && !s.subAccountId);
1057
+ const action = await select({
1058
+ message: 'What would you like to do?',
1059
+ choices: [
1060
+ { name: chalk.green('Create new strategy'), value: 'create' },
1061
+ ...(unbound.length > 0
1062
+ ? [{ name: `Attach unbound strategy (${unbound.length} available)`, value: 'attach' }]
1063
+ : []),
1064
+ { name: 'Back', value: 'back' },
1065
+ ],
1066
+ });
1067
+ if (action === 'back')
1068
+ return;
1069
+ if (action === 'create') {
1070
+ await createNewStrategy(creds.accessToken, supported, walletId, wallet);
1071
+ return;
1072
+ }
1073
+ if (action === 'attach' && unbound.length > 0) {
1074
+ const picked = await select({
1075
+ message: 'Select strategy to attach:',
1076
+ choices: unbound.map((s) => ({
1077
+ name: `${strategyDisplayName(s)} ${s.symbols?.join(', ') ?? '—'} ${s.active ? chalk.green('ON') : chalk.dim('OFF')}`,
1078
+ value: s,
1079
+ })),
1080
+ });
1081
+ info(`Strategy: ${picked.strategyId} — ${strategyDisplayName(picked)} (${picked.symbols?.join(', ')})`);
1082
+ return;
1083
+ }
1084
+ return;
1085
+ }
1086
+ // ── Show all strategies overview ─────────────────────────────────
1087
+ console.log('');
1088
+ console.log(chalk.bold(`Autopilot Strategies — ${getSubAccountLabel(wallet)} (${walletStrategies.length}):`));
1089
+ console.log('');
1090
+ for (const s of walletStrategies) {
1091
+ const statusIcon = s.active ? chalk.green('● ON') : chalk.dim('○ OFF');
1092
+ const nameLabel = chalk.bold(strategyDisplayName(s));
1093
+ const symLabel = s.symbols && s.symbols.length > 0 ? s.symbols.join(', ') : chalk.dim('no symbols');
1094
+ console.log(` ${statusIcon} ${nameLabel} — ${symLabel}`);
1095
+ if (s.strategyId) {
1096
+ console.log(` ID: ${chalk.dim(s.strategyId)}`);
1097
+ }
1098
+ if (s.createdAt) {
1099
+ console.log(` Created: ${chalk.dim(s.createdAt)}`);
1100
+ }
1101
+ }
1102
+ console.log('');
1103
+ // ── Pick which strategy to manage ────────────────────────────────
1104
+ let state;
1105
+ if (walletStrategies.length === 1) {
1106
+ state = walletStrategies[0];
1107
+ }
1108
+ else {
1109
+ state = await select({
1110
+ message: 'Select strategy to manage:',
1111
+ choices: [
1112
+ ...walletStrategies.map((s) => ({
1113
+ name: `${s.active ? chalk.green('●') : chalk.dim('○')} ${strategyDisplayName(s)} ${s.symbols?.join(', ') ?? '—'}`,
1114
+ value: s,
1115
+ })),
1116
+ { name: chalk.green('+ Create new strategy'), value: { active: false } },
1117
+ ],
1118
+ });
1119
+ if (!state.strategyId) {
1120
+ await createNewStrategy(creds.accessToken, supported, walletId, wallet);
1121
+ return;
1122
+ }
1123
+ }
1124
+ // ── Strategy dashboard ───────────────────────────────────────────
1125
+ await showAutopilotDashboard(creds.accessToken, wallet, state);
1126
+ // ── Action menu loop ─────────────────────────────────────────────
1127
+ let keepGoing = true;
1128
+ while (keepGoing) {
1129
+ const statusLabel = state.active ? chalk.green('ON') : chalk.dim('OFF');
1130
+ const action = await select({
1131
+ message: `${strategyDisplayName(state)} [${statusLabel}] — What would you like to do?`,
1132
+ choices: [
1133
+ ...(state.active
1134
+ ? [{ name: chalk.red('Turn OFF autopilot'), value: 'off' }]
1135
+ : [{ name: chalk.green('Turn ON autopilot'), value: 'on' }]),
1136
+ { name: 'Update symbols', value: 'update-symbols' },
1137
+ { name: 'Update strategy config', value: 'update-config' },
1138
+ { name: 'View performance', value: 'perf' },
1139
+ { name: 'View trading records', value: 'records' },
1140
+ { name: 'Back', value: 'back' },
1141
+ ],
1142
+ });
1143
+ switch (action) {
1144
+ case 'back':
1145
+ keepGoing = false;
1146
+ break;
1147
+ case 'on':
1148
+ if (state.strategyId) {
1149
+ const spin = spinner('Enabling autopilot…');
1150
+ const res = await perpsApi.enableStrategy(creds.accessToken, state.strategyId);
1151
+ spin.stop();
1152
+ assertApiOk(res, 'Failed to enable autopilot');
1153
+ state.active = true;
1154
+ success(`${strategyDisplayName(state)} is now ON`);
1155
+ }
1156
+ break;
1157
+ case 'off':
1158
+ if (state.strategyId) {
1159
+ const ok = await confirm({ message: `Turn off ${strategyDisplayName(state)}? AI will stop trading.`, default: false });
1160
+ if (!ok)
1161
+ break;
1162
+ const spin = spinner('Disabling autopilot…');
1163
+ const res = await perpsApi.disableStrategy(creds.accessToken, state.strategyId);
1164
+ spin.stop();
1165
+ assertApiOk(res, 'Failed to disable autopilot');
1166
+ state.active = false;
1167
+ success(`${strategyDisplayName(state)} is now OFF`);
1168
+ }
1169
+ break;
1170
+ case 'update-symbols': {
1171
+ info(`Supported: ${supported.join(', ')} | Current: ${state.symbols?.join(', ') ?? 'none'}`);
1172
+ const symbolsInput = await input({
1173
+ message: 'New symbols (comma-separated):',
1174
+ default: state.symbols?.join(',') ?? '',
1175
+ });
1176
+ const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
1177
+ const spin = spinner('Updating symbols…');
1178
+ const res = await perpsApi.updateStrategy(creds.accessToken, {
1179
+ strategyId: state.strategyId,
1180
+ symbols,
1181
+ strategyConfig: state.strategyConfig,
1182
+ language: state.language,
1183
+ });
1184
+ spin.stop();
1185
+ assertApiOk(res, 'Failed to update symbols');
1186
+ state.symbols = symbols;
1187
+ success(`Symbols updated: ${symbols.join(', ')}`);
1188
+ break;
1189
+ }
1190
+ case 'update-config': {
1191
+ console.log('');
1192
+ console.log(chalk.bold('Current strategy config:'));
1193
+ if (state.strategyConfig && Object.keys(state.strategyConfig).length > 0) {
1194
+ printKV(state.strategyConfig);
1195
+ }
1196
+ else {
1197
+ console.log(chalk.dim(' No custom config set.'));
1198
+ }
1199
+ console.log('');
1200
+ const configJson = await input({
1201
+ message: 'New config (JSON, or press Enter to keep current):',
1202
+ default: state.strategyConfig ? JSON.stringify(state.strategyConfig) : '{}',
1203
+ });
1204
+ let newConfig;
1205
+ try {
1206
+ newConfig = JSON.parse(configJson);
1207
+ }
1208
+ catch {
1209
+ warn('Invalid JSON. Config not updated.');
594
1210
  break;
595
1211
  }
1212
+ const spin = spinner('Updating config…');
1213
+ const res = await perpsApi.updateStrategy(creds.accessToken, {
1214
+ strategyId: state.strategyId,
1215
+ symbols: state.symbols ?? [],
1216
+ strategyConfig: newConfig,
1217
+ language: state.language,
1218
+ });
1219
+ spin.stop();
1220
+ assertApiOk(res, 'Failed to update config');
1221
+ state.strategyConfig = newConfig;
1222
+ success('Strategy config updated.');
1223
+ break;
1224
+ }
1225
+ case 'perf': {
1226
+ const spin = spinner('Fetching performance…');
1227
+ const res = await perpsApi.getPerformanceMetrics(creds.accessToken);
1228
+ spin.stop();
1229
+ if (res.success && res.data) {
1230
+ const ap = state.strategyConfig?.pattern !== undefined
1231
+ ? String(state.strategyConfig.pattern) : undefined;
1232
+ console.log('');
1233
+ console.log(chalk.bold(`Performance — ${strategyDisplayName(state)}:`));
1234
+ printPerformanceData(res.data, ap);
1235
+ console.log('');
1236
+ }
1237
+ else {
1238
+ console.log(chalk.dim(' No performance data available.'));
1239
+ }
1240
+ break;
1241
+ }
1242
+ case 'records': {
1243
+ const spin = spinner('Fetching records…');
1244
+ const res = await perpsApi.getRecords(creds.accessToken, 1, 20);
1245
+ spin.stop();
1246
+ if (res.success && Array.isArray(res.data) && res.data.length > 0) {
1247
+ console.log('');
1248
+ console.log(chalk.bold('Recent Autopilot Records:'));
1249
+ printTable(res.data);
1250
+ console.log('');
1251
+ }
1252
+ else {
1253
+ console.log(chalk.dim(' No autopilot records.'));
1254
+ }
1255
+ break;
596
1256
  }
597
1257
  }
598
1258
  }
599
- if (strategies.length === 0)
600
- return { active: false };
601
- const s = strategies[0];
602
- // Check all possible status field names
603
- const status = String(s.status ?? s.state ?? s.isActive ?? s.enabled ?? '').toLowerCase();
604
- const isActive = status === 'active' || status === 'enabled' || status === 'running'
605
- || status === 'true' || s.isActive === true || s.enabled === true;
606
- return {
607
- active: isActive,
608
- strategyId: String(s._id ?? s.id ?? s.strategyId ?? ''),
609
- symbols: Array.isArray(s.symbols) ? s.symbols : [],
610
- };
1259
+ }));
1260
+ async function createNewStrategy(token, supported, walletId, wallet) {
1261
+ info(`Supported symbols: ${supported.join(', ')}`);
1262
+ const symbolsInput = await input({
1263
+ message: 'Symbols to trade (comma-separated):',
1264
+ default: supported.slice(0, 3).join(','),
1265
+ });
1266
+ const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
1267
+ const configInput = await input({
1268
+ message: 'Strategy config (JSON, or press Enter for default):',
1269
+ default: '{}',
1270
+ });
1271
+ let strategyConfig;
1272
+ try {
1273
+ const parsed = JSON.parse(configInput);
1274
+ if (Object.keys(parsed).length > 0)
1275
+ strategyConfig = parsed;
1276
+ }
1277
+ catch {
1278
+ warn('Invalid JSON — using default config.');
1279
+ }
1280
+ const spin = spinner('Creating autopilot strategy…');
1281
+ const res = await perpsApi.createStrategy(token, {
1282
+ symbols,
1283
+ subAccountId: walletId || undefined,
1284
+ strategyConfig,
1285
+ });
1286
+ spin.stop();
1287
+ assertApiOk(res, 'Failed to create autopilot strategy');
1288
+ success(`Autopilot created for ${symbols.join(', ')} on ${getSubAccountLabel(wallet)}!`);
611
1289
  }
612
- // ─── autopilot ──────────────────────────────────────────────────────────
613
- const autopilotCmd = new Command('autopilot')
614
- .alias('ap')
615
- .description('Manage AI autopilot trading strategy')
616
- .action(wrapAction(async () => {
617
- const creds = requireAuth();
618
- const statusSpin = spinner('Checking autopilot status…');
619
- const state = await getAutopilotState(creds.accessToken);
620
- statusSpin.stop();
1290
+ async function showAutopilotDashboard(token, wallet, state) {
621
1291
  const statusLabel = state.active ? chalk.green.bold('ON') : chalk.dim('OFF');
1292
+ const nameLabel = strategyDisplayName(state);
622
1293
  console.log('');
623
- console.log(chalk.bold('Autopilot Status:') + ` ${statusLabel}`);
624
- if (state.symbols && state.symbols.length > 0) {
625
- console.log(` Symbols : ${state.symbols.join(', ')}`);
1294
+ console.log(chalk.bold(`Strategy: ${nameLabel}`) + ` ${statusLabel} (${getSubAccountLabel(wallet)})`);
1295
+ console.log('');
1296
+ // ── Basic info ──────────────────────────────────────────────────
1297
+ const infoRows = [];
1298
+ if (state.strategyId)
1299
+ infoRows.push(['ID', chalk.dim(state.strategyId)]);
1300
+ if (state.symbols && state.symbols.length > 0)
1301
+ infoRows.push(['Symbols', state.symbols.join(', ')]);
1302
+ if (state.language)
1303
+ infoRows.push(['Language', state.language]);
1304
+ if (state.createdAt)
1305
+ infoRows.push(['Created', chalk.dim(formatDateStr(state.createdAt))]);
1306
+ if (state.updatedAt)
1307
+ infoRows.push(['Updated', chalk.dim(formatDateStr(state.updatedAt))]);
1308
+ const activePattern = state.strategyConfig?.pattern !== undefined
1309
+ ? String(state.strategyConfig.pattern) : undefined;
1310
+ if (activePattern)
1311
+ infoRows.push(['Using', chalk.cyan.bold(`Strategy ${activePattern}`)]);
1312
+ if (infoRows.length > 0) {
1313
+ const maxLabel = Math.max(...infoRows.map(([l]) => l.length));
1314
+ for (const [label, val] of infoRows) {
1315
+ console.log(` ${label.padEnd(maxLabel)} : ${val}`);
1316
+ }
1317
+ }
1318
+ // ── Strategy Config ─────────────────────────────────────────────
1319
+ if (state.strategyConfig && Object.keys(state.strategyConfig).length > 0) {
1320
+ console.log('');
1321
+ console.log(chalk.bold(' Config:'));
1322
+ for (const [k, v] of Object.entries(state.strategyConfig)) {
1323
+ if (k === 'pattern')
1324
+ continue;
1325
+ if (v && typeof v === 'object') {
1326
+ console.log(` ${chalk.dim(k)}:`);
1327
+ for (const [ik, iv] of Object.entries(v)) {
1328
+ console.log(` ${ik.padEnd(20)} : ${chalk.cyan(String(iv))}`);
1329
+ }
1330
+ }
1331
+ else {
1332
+ console.log(` ${chalk.dim(k).padEnd(24)} : ${chalk.cyan(String(v))}`);
1333
+ }
1334
+ }
1335
+ }
1336
+ // ── Performance ─────────────────────────────────────────────────
1337
+ const perfSpin = spinner('Fetching performance…');
1338
+ const perfRes = await perpsApi.getPerformanceMetrics(token);
1339
+ perfSpin.stop();
1340
+ if (perfRes.success && perfRes.data) {
1341
+ console.log('');
1342
+ console.log(chalk.bold(' Performance (all strategies):'));
1343
+ printPerformanceData(perfRes.data, activePattern);
626
1344
  }
627
1345
  console.log('');
628
- const action = await select({
629
- message: 'What would you like to do?',
630
- choices: [
631
- ...(state.active
632
- ? [{ name: chalk.red('Turn OFF autopilot'), value: 'off' }]
633
- : [{ name: chalk.green('Turn ON autopilot'), value: 'on' }]),
634
- ...(!state.strategyId ? [{ name: 'Create autopilot strategy', value: 'create' }] : []),
635
- ...(state.strategyId ? [{ name: 'Update symbols', value: 'update' }] : []),
636
- { name: 'View performance', value: 'perf' },
637
- { name: 'Back', value: 'back' },
638
- ],
639
- });
640
- if (action === 'back')
1346
+ }
1347
+ function formatDateStr(d) {
1348
+ try {
1349
+ const date = new Date(d);
1350
+ if (isNaN(date.getTime()))
1351
+ return d;
1352
+ return date.toLocaleString('en-US', {
1353
+ year: 'numeric', month: 'short', day: 'numeric',
1354
+ hour: '2-digit', minute: '2-digit',
1355
+ });
1356
+ }
1357
+ catch {
1358
+ return d;
1359
+ }
1360
+ }
1361
+ /**
1362
+ * Render performance metrics. Handles two formats:
1363
+ * 1. Pattern-based: { "1": { estAPR, tradesCount }, "2": { ... }, ... }
1364
+ * 2. Flat: { totalPnl, winRate, ... }
1365
+ *
1366
+ * @param activePattern - If set, highlights the column for this pattern ID (e.g. "5")
1367
+ */
1368
+ function printPerformanceData(data, activePattern) {
1369
+ const entries = Object.entries(data);
1370
+ if (entries.length === 0) {
1371
+ console.log(chalk.dim(' No data.'));
641
1372
  return;
642
- if (action === 'on' && state.strategyId) {
643
- const spin = spinner('Enabling autopilot…');
644
- const res = await perpsApi.enableStrategy(creds.accessToken, state.strategyId);
645
- spin.stop();
646
- assertApiOk(res, 'Failed to enable autopilot');
647
- success('Autopilot is now ON');
1373
+ }
1374
+ // Detect pattern-based format: keys are numeric, values are objects
1375
+ const isPatternBased = entries.every(([k, v]) => /^\d+$/.test(k) && v && typeof v === 'object');
1376
+ if (isPatternBased) {
1377
+ // Collect all metric keys across all patterns
1378
+ const allKeys = new Set();
1379
+ for (const [, v] of entries) {
1380
+ for (const mk of Object.keys(v))
1381
+ allKeys.add(mk);
1382
+ }
1383
+ const metricKeys = Array.from(allKeys);
1384
+ // Header
1385
+ const patternIds = entries.map(([k]) => k);
1386
+ const colWidth = 16;
1387
+ const labelCol = 16;
1388
+ const header = ' '
1389
+ + chalk.dim('Metric'.padEnd(labelCol))
1390
+ + patternIds.map((id) => {
1391
+ const label = `Strategy ${id}`;
1392
+ if (id === activePattern)
1393
+ return chalk.bold.cyan(`${label} ★`.padStart(colWidth));
1394
+ return chalk.bold(label.padStart(colWidth));
1395
+ }).join('');
1396
+ console.log(header);
1397
+ console.log(' ' + chalk.dim('─'.repeat(labelCol + colWidth * patternIds.length)));
1398
+ // Rows
1399
+ const metricLabels = {
1400
+ estAPR: 'Est. APR',
1401
+ tradesCount: 'Trades',
1402
+ pnl: 'PnL',
1403
+ winRate: 'Win Rate',
1404
+ sharpeRatio: 'Sharpe',
1405
+ maxDrawdown: 'Max DD',
1406
+ };
1407
+ for (const mk of metricKeys) {
1408
+ const label = (metricLabels[mk] ?? mk).padEnd(labelCol);
1409
+ const cells = entries.map(([id, v]) => {
1410
+ const val = v[mk];
1411
+ if (val === undefined || val === null)
1412
+ return chalk.dim('—'.padStart(colWidth));
1413
+ const num = Number(val);
1414
+ const isActive = id === activePattern;
1415
+ if (mk === 'estAPR' || mk.toLowerCase().includes('apr')) {
1416
+ const color = isActive ? chalk.cyan.bold : (num >= 0 ? chalk.green : chalk.red);
1417
+ return color(`${num.toFixed(2)}%`.padStart(colWidth));
1418
+ }
1419
+ if (mk.toLowerCase().includes('trades') || mk.toLowerCase().includes('count')) {
1420
+ const color = isActive ? chalk.cyan.bold : chalk.white;
1421
+ return color(num.toLocaleString().padStart(colWidth));
1422
+ }
1423
+ if (mk.toLowerCase().includes('pnl')) {
1424
+ return pnlFmt(num).padStart(colWidth);
1425
+ }
1426
+ return String(val).padStart(colWidth);
1427
+ });
1428
+ console.log(` ${chalk.dim(label)}${cells.join('')}`);
1429
+ }
648
1430
  return;
649
1431
  }
650
- if (action === 'off' && state.strategyId) {
651
- const ok = await confirm({ message: 'Turn off autopilot? AI will stop trading.', default: false });
652
- if (!ok)
653
- return;
654
- const spin = spinner('Disabling autopilot…');
655
- const res = await perpsApi.disableStrategy(creds.accessToken, state.strategyId);
656
- spin.stop();
657
- assertApiOk(res, 'Failed to disable autopilot');
658
- success('Autopilot is now OFF');
659
- return;
660
- }
661
- if (action === 'create' || (action === 'on' && !state.strategyId)) {
662
- const symSpin = spinner('Fetching supported symbols…');
663
- const symRes = await perpsApi.getSupportedSymbols(creds.accessToken);
664
- symSpin.stop();
665
- const supported = symRes.success && Array.isArray(symRes.data) ? symRes.data : ['BTC', 'ETH', 'SOL'];
666
- info(`Supported symbols: ${supported.join(', ')}`);
667
- const symbolsInput = await input({
668
- message: 'Symbols to trade (comma-separated):',
669
- default: supported.slice(0, 3).join(','),
670
- });
671
- const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
672
- const spin = spinner('Creating autopilot strategy…');
673
- const res = await perpsApi.createStrategy(creds.accessToken, { symbols });
674
- spin.stop();
675
- assertApiOk(res, 'Failed to create autopilot strategy');
676
- success(`Autopilot created for ${symbols.join(', ')} and enabled!`);
677
- return;
678
- }
679
- if (action === 'update' && state.strategyId) {
680
- const symSpin = spinner('Fetching supported symbols…');
681
- const symRes = await perpsApi.getSupportedSymbols(creds.accessToken);
682
- symSpin.stop();
683
- const supported = symRes.success && Array.isArray(symRes.data) ? symRes.data : ['BTC', 'ETH', 'SOL'];
684
- info(`Supported: ${supported.join(', ')} | Current: ${state.symbols?.join(', ') ?? 'none'}`);
685
- const symbolsInput = await input({
686
- message: 'New symbols (comma-separated):',
687
- default: state.symbols?.join(',') ?? '',
688
- });
689
- const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
690
- const spin = spinner('Updating strategy…');
691
- const res = await perpsApi.updateStrategy(creds.accessToken, { strategyId: state.strategyId, symbols });
692
- spin.stop();
693
- assertApiOk(res, 'Failed to update strategy');
694
- success(`Autopilot updated: ${symbols.join(', ')}`);
1432
+ // Flat format: render as key-value pairs with smart formatting
1433
+ const flatFields = [
1434
+ ['totalPnl', 'Total PnL'],
1435
+ ['totalPnlPercent', 'Total PnL %'],
1436
+ ['unrealizedPnl', 'Unrl. PnL'],
1437
+ ['realizedPnl', 'Realized PnL'],
1438
+ ['winRate', 'Win Rate'],
1439
+ ['totalTrades', 'Total Trades'],
1440
+ ['sharpeRatio', 'Sharpe Ratio'],
1441
+ ['maxDrawdown', 'Max Drawdown'],
1442
+ ['estAPR', 'Est. APR'],
1443
+ ['tradesCount', 'Trades'],
1444
+ ];
1445
+ const knownKeys = new Set(flatFields.map(([k]) => k));
1446
+ const allFields = [
1447
+ ...flatFields,
1448
+ ...entries.filter(([k]) => !knownKeys.has(k)).map(([k]) => [k, k]),
1449
+ ];
1450
+ for (const [key, label] of allFields) {
1451
+ const v = data[key];
1452
+ if (v === undefined || v === null)
1453
+ continue;
1454
+ if (typeof v === 'object') {
1455
+ console.log(` ${chalk.dim(label)}`);
1456
+ for (const [ik, iv] of Object.entries(v)) {
1457
+ console.log(` ${ik.padEnd(18)} : ${chalk.cyan(String(iv))}`);
1458
+ }
1459
+ continue;
1460
+ }
1461
+ const num = Number(v);
1462
+ let display;
1463
+ if (key.includes('Pnl') && !key.includes('Percent')) {
1464
+ display = pnlFmt(num);
1465
+ }
1466
+ else if (key.includes('Percent') || key === 'winRate' || key === 'maxDrawdown') {
1467
+ const color = num >= 0 ? chalk.green : chalk.red;
1468
+ display = color(`${num >= 0 ? '+' : ''}${num.toFixed(2)}%`);
1469
+ }
1470
+ else if (key.toLowerCase().includes('apr')) {
1471
+ const color = num >= 0 ? chalk.green : chalk.red;
1472
+ display = color(`${num.toFixed(2)}%`);
1473
+ }
1474
+ else if (key.toLowerCase().includes('trades') || key.toLowerCase().includes('count')) {
1475
+ display = num.toLocaleString();
1476
+ }
1477
+ else {
1478
+ display = String(v);
1479
+ }
1480
+ console.log(` ${chalk.dim(label.padEnd(14))} : ${display}`);
1481
+ }
1482
+ }
1483
+ // ─── wallets (list all sub-wallets) ─────────────────────────────────────
1484
+ const walletsCmd = new Command('wallets')
1485
+ .alias('w')
1486
+ .description('List all perps sub-wallets with balances, positions, and autopilot status')
1487
+ .action(wrapAction(async () => {
1488
+ const creds = requireAuth();
1489
+ const spin = spinner('Fetching wallets…');
1490
+ const [wallets, allStates] = await Promise.all([
1491
+ fetchSubAccounts(creds.accessToken),
1492
+ getAllAutopilotStates(creds.accessToken),
1493
+ ]);
1494
+ spin.stop();
1495
+ if (wallets.length === 0) {
1496
+ info('No perps wallets found. Create one with: minara perps create-wallet');
695
1497
  return;
696
1498
  }
697
- if (action === 'perf') {
698
- const spin = spinner('Fetching performance…');
699
- const res = await perpsApi.getPerformanceMetrics(creds.accessToken);
700
- spin.stop();
701
- if (res.success && res.data) {
702
- console.log('');
703
- console.log(chalk.bold('Autopilot Performance:'));
704
- printKV(res.data);
705
- console.log('');
1499
+ console.log('');
1500
+ console.log(chalk.bold(`Perps Wallets (${wallets.length}):`));
1501
+ console.log('');
1502
+ // Fetch per-wallet summaries in parallel for accurate financial data
1503
+ const summaryResults = await Promise.all(wallets.map((w) => perpsApi.getSubAccountSummary(creds.accessToken, getSubAccountId(w))));
1504
+ for (let i = 0; i < wallets.length; i++) {
1505
+ const w = wallets[i];
1506
+ const sumRes = summaryResults[i];
1507
+ const raw = sumRes.success && sumRes.data
1508
+ ? sumRes.data
1509
+ : w;
1510
+ const s = normalizeWalletSummary(raw);
1511
+ const wId = getSubAccountId(w);
1512
+ const wStrategies = getAllStrategiesForWallet(allStates, wId, !!w.isDefault);
1513
+ const activeCount = wStrategies.filter((st) => st.active).length;
1514
+ let apLabel;
1515
+ if (wStrategies.length === 0) {
1516
+ apLabel = chalk.dim('[No AP]');
1517
+ }
1518
+ else if (activeCount > 0) {
1519
+ apLabel = chalk.green(`[${activeCount}/${wStrategies.length} AP ON]`);
706
1520
  }
707
1521
  else {
708
- console.log(chalk.dim(' No performance data available.'));
1522
+ apLabel = chalk.dim(`[${wStrategies.length} AP OFF]`);
1523
+ }
1524
+ const defLabel = w.isDefault ? chalk.cyan(' (default)') : '';
1525
+ console.log(` ${chalk.bold(w.name ?? 'Unnamed')}${defLabel} ${apLabel}`);
1526
+ if (w.address) {
1527
+ console.log(` Address : ${chalk.yellow(w.address)}`);
1528
+ }
1529
+ console.log(` Equity : ${fmt(s.equity)}`);
1530
+ console.log(` Available : ${fmt(s.available)}`);
1531
+ console.log(` Margin : ${fmt(s.margin)}`);
1532
+ console.log(` Unrl. PnL : ${pnlFmt(s.unrealizedPnl)}`);
1533
+ if (wStrategies.length > 0) {
1534
+ const apNames = wStrategies.map((st) => `${strategyDisplayName(st)} (${st.symbols?.join(', ') ?? '—'})${st.active ? chalk.green(' ON') : ''}`);
1535
+ console.log(` Strategies: ${apNames.join(' | ')}`);
709
1536
  }
1537
+ if (s.positions.length > 0) {
1538
+ console.log(` Positions : ${s.positions.length} open`);
1539
+ }
1540
+ console.log('');
710
1541
  }
1542
+ // Aggregated summary
1543
+ const aggSpin = spinner('Fetching aggregated summary…');
1544
+ const aggRes = await perpsApi.getAggregatedSummary(creds.accessToken);
1545
+ aggSpin.stop();
1546
+ if (aggRes.success && aggRes.data) {
1547
+ const agg = aggRes.data;
1548
+ const aggS = normalizeWalletSummary(agg);
1549
+ console.log(chalk.bold('Aggregated Summary:'));
1550
+ console.log(` Total Equity : ${fmt(aggS.equity || Number(agg.totalEquity ?? 0))}`);
1551
+ console.log(` Total Unrl. PnL : ${pnlFmt(aggS.unrealizedPnl || Number(agg.totalUnrealizedPnl ?? 0))}`);
1552
+ console.log(` Total Margin : ${fmt(aggS.margin || Number(agg.totalMarginUsed ?? 0))}`);
1553
+ console.log('');
1554
+ }
1555
+ }));
1556
+ // ─── create-wallet ──────────────────────────────────────────────────────
1557
+ const createWalletCmd = new Command('create-wallet')
1558
+ .description('Create a new perps sub-wallet')
1559
+ .option('-n, --name <name>', 'Wallet name (max 20 chars)')
1560
+ .action(wrapAction(async (opts) => {
1561
+ const creds = requireAuth();
1562
+ const name = opts.name ?? await input({
1563
+ message: 'Wallet name (max 20 characters):',
1564
+ validate: (v) => v.length > 0 && v.length <= 20 ? true : 'Name must be 1–20 characters',
1565
+ });
1566
+ const spin = spinner('Creating wallet…');
1567
+ const res = await perpsApi.createSubAccount(creds.accessToken, { name });
1568
+ spin.stop();
1569
+ assertApiOk(res, 'Failed to create wallet');
1570
+ success(`Wallet "${name}" created!`);
1571
+ if (res.data?.address) {
1572
+ console.log(` Address: ${chalk.yellow(res.data.address)}`);
1573
+ }
1574
+ }));
1575
+ // ─── rename-wallet ──────────────────────────────────────────────────────
1576
+ const renameWalletCmd = new Command('rename-wallet')
1577
+ .description('Rename a perps sub-wallet')
1578
+ .action(wrapAction(async () => {
1579
+ const creds = requireAuth();
1580
+ const spin = spinner('Loading wallets…');
1581
+ const wallet = await pickSubAccount(creds.accessToken, 'Select wallet to rename:');
1582
+ spin.stop();
1583
+ if (!wallet)
1584
+ return;
1585
+ const newName = await input({
1586
+ message: `New name for "${wallet.name ?? 'Unnamed'}" (max 10 chars):`,
1587
+ validate: (v) => v.length > 0 && v.length <= 10 ? true : 'Name must be 1–10 characters',
1588
+ });
1589
+ const renameSpin = spinner('Renaming…');
1590
+ const res = await perpsApi.renameSubAccount(creds.accessToken, {
1591
+ subAccountId: getSubAccountId(wallet),
1592
+ name: newName,
1593
+ });
1594
+ renameSpin.stop();
1595
+ assertApiOk(res, 'Failed to rename wallet');
1596
+ success(`Wallet renamed to "${newName}"`);
1597
+ }));
1598
+ // ─── sweep (consolidate sub-wallet funds to default) ────────────────────
1599
+ const sweepCmd = new Command('sweep')
1600
+ .description('Consolidate funds from a sub-wallet to the default wallet')
1601
+ .option('-y, --yes', 'Skip confirmation')
1602
+ .action(wrapAction(async (opts) => {
1603
+ const creds = requireAuth();
1604
+ const loadSpin = spinner('Loading wallets & strategies…');
1605
+ const [wallets, allStates] = await Promise.all([
1606
+ fetchSubAccounts(creds.accessToken),
1607
+ getAllAutopilotStates(creds.accessToken),
1608
+ ]);
1609
+ const nonDefault = wallets.filter((w) => !w.isDefault);
1610
+ if (nonDefault.length === 0) {
1611
+ loadSpin.stop();
1612
+ info('No sub-wallets to sweep from. Only the default wallet exists.');
1613
+ return;
1614
+ }
1615
+ const summaries = await Promise.all(nonDefault.map((w) => perpsApi.getSubAccountSummary(creds.accessToken, getSubAccountId(w))));
1616
+ loadSpin.stop();
1617
+ const wallet = await select({
1618
+ message: 'Select sub-wallet to sweep funds FROM:',
1619
+ choices: nonDefault.map((w, i) => {
1620
+ const wId = getSubAccountId(w);
1621
+ const wStrategies = getAllStrategiesForWallet(allStates, wId, !!w.isDefault);
1622
+ const hasActive = wStrategies.some((s) => s.active);
1623
+ const apLabel = hasActive ? chalk.red(' [AP ON — cannot sweep]') : '';
1624
+ const raw = summaries[i].success && summaries[i].data
1625
+ ? summaries[i].data : w;
1626
+ const s = normalizeWalletSummary(raw);
1627
+ const eq = fmt(s.available);
1628
+ return {
1629
+ name: `${getSubAccountLabel(w)} ${chalk.dim(eq)}${apLabel}`,
1630
+ value: w,
1631
+ };
1632
+ }),
1633
+ });
1634
+ const walletId = getSubAccountId(wallet);
1635
+ const sweepStrategies = getAllStrategiesForWallet(allStates, walletId, !!wallet.isDefault);
1636
+ const activeAp = sweepStrategies.find((s) => s.active);
1637
+ if (activeAp) {
1638
+ console.log('');
1639
+ warn(`Autopilot "${strategyDisplayName(activeAp)}" is ON for "${wallet.name ?? 'Unnamed'}". You must turn it off before sweeping funds.`);
1640
+ info('Run: minara perps autopilot → select this wallet → Turn OFF');
1641
+ console.log('');
1642
+ return;
1643
+ }
1644
+ const equity = Number(wallet.equityValue ?? 0);
1645
+ if (equity <= 0) {
1646
+ info('This wallet has no funds to sweep.');
1647
+ return;
1648
+ }
1649
+ console.log('');
1650
+ console.log(chalk.bold('Sweep Funds:'));
1651
+ console.log(` From : ${getSubAccountLabel(wallet)}`);
1652
+ console.log(` To : Default wallet`);
1653
+ console.log(` Amount : ${fmt(equity)} (all available)`);
1654
+ console.log('');
1655
+ if (!opts.yes) {
1656
+ await requireTransactionConfirmation(`Sweep funds from "${wallet.name}" to default wallet`);
1657
+ }
1658
+ await requireTouchId();
1659
+ const spin = spinner('Sweeping funds…');
1660
+ const res = await perpsApi.sweepFunds(creds.accessToken, { subAccountId: walletId });
1661
+ spin.stop();
1662
+ assertApiOk(res, 'Sweep failed');
1663
+ success(`Funds swept from "${wallet.name}" to default wallet`);
1664
+ printTxResult(res.data);
1665
+ }));
1666
+ // ─── transfer (between sub-wallets) ─────────────────────────────────────
1667
+ const transferCmd = new Command('transfer')
1668
+ .description('Transfer USDC between perps sub-wallets')
1669
+ .option('-a, --amount <amount>', 'USDC amount')
1670
+ .option('-y, --yes', 'Skip confirmation')
1671
+ .action(wrapAction(async (opts) => {
1672
+ const creds = requireAuth();
1673
+ const spin = spinner('Loading wallets…');
1674
+ const wallets = await fetchSubAccounts(creds.accessToken);
1675
+ if (wallets.length < 2) {
1676
+ spin.stop();
1677
+ info('You need at least 2 wallets to transfer between them. Create one with: minara perps create-wallet');
1678
+ return;
1679
+ }
1680
+ const summaries = await Promise.all(wallets.map((w) => perpsApi.getSubAccountSummary(creds.accessToken, getSubAccountId(w))));
1681
+ spin.stop();
1682
+ const walletLabel = (w, i) => {
1683
+ const raw = summaries[i].success && summaries[i].data
1684
+ ? summaries[i].data : w;
1685
+ const s = normalizeWalletSummary(raw);
1686
+ return `${getSubAccountLabel(w)} ${chalk.dim(fmt(s.available))}`;
1687
+ };
1688
+ const from = await select({
1689
+ message: 'Transfer FROM:',
1690
+ choices: wallets.map((w, i) => ({
1691
+ name: walletLabel(w, i),
1692
+ value: w,
1693
+ })),
1694
+ });
1695
+ const toChoices = wallets
1696
+ .map((w, i) => ({ w, i }))
1697
+ .filter(({ w }) => getSubAccountId(w) !== getSubAccountId(from));
1698
+ const to = await select({
1699
+ message: 'Transfer TO:',
1700
+ choices: toChoices.map(({ w, i }) => ({
1701
+ name: walletLabel(w, i),
1702
+ value: w,
1703
+ })),
1704
+ });
1705
+ const amount = opts.amount
1706
+ ? parseFloat(opts.amount)
1707
+ : await numberPrompt({ message: 'USDC amount to transfer:', min: 0.01, required: true });
1708
+ if (!amount || amount <= 0) {
1709
+ warn('Invalid amount.');
1710
+ return;
1711
+ }
1712
+ console.log('');
1713
+ console.log(chalk.bold('Transfer:'));
1714
+ console.log(` From : ${getSubAccountLabel(from)}`);
1715
+ console.log(` To : ${getSubAccountLabel(to)}`);
1716
+ console.log(` Amount : ${fmt(amount)}`);
1717
+ console.log('');
1718
+ if (!opts.yes) {
1719
+ await requireTransactionConfirmation(`Transfer ${amount} USDC`);
1720
+ }
1721
+ await requireTouchId();
1722
+ const transferSpin = spinner('Transferring…');
1723
+ const fromId = from.isDefault ? undefined : getSubAccountId(from);
1724
+ const toId = to.isDefault ? undefined : getSubAccountId(to);
1725
+ const res = await perpsApi.transferFunds(creds.accessToken, {
1726
+ fromSubAccountId: fromId,
1727
+ toSubAccountId: toId,
1728
+ amount,
1729
+ });
1730
+ transferSpin.stop();
1731
+ assertApiOk(res, 'Transfer failed');
1732
+ success(`Transferred ${amount} USDC from "${from.name}" to "${to.name}"`);
1733
+ printTxResult(res.data);
711
1734
  }));
712
1735
  // ─── ask (long/short analysis) ──────────────────────────────────────────
713
1736
  const askCmd = new Command('ask')
714
1737
  .description('Get AI trading analysis for an asset (long/short recommendation)')
715
- .action(wrapAction(async () => {
1738
+ .option(WALLET_OPT[0], WALLET_OPT[1])
1739
+ .action(wrapAction(async (opts) => {
716
1740
  const creds = requireAuth();
717
1741
  const dataSpin = spinner('Fetching assets…');
718
1742
  const assets = await perpsApi.getAssetMeta();
@@ -786,10 +1810,17 @@ const askCmd = new Command('ask')
786
1810
  const doQuick = await confirm({ message: 'Place this order now?', default: false });
787
1811
  if (!doQuick)
788
1812
  return;
789
- // Check autopilot before placing
790
- const apState = await getAutopilotState(creds.accessToken);
791
- if (apState.active) {
792
- warn('Autopilot is ON manual orders are disabled while AI is trading.');
1813
+ const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Place order on which wallet?');
1814
+ if (!resolved)
1815
+ return;
1816
+ const { wallet: orderWallet, walletId: orderWalletId } = resolved;
1817
+ // Check autopilot for this wallet before placing
1818
+ const allStates = await getAllAutopilotStates(creds.accessToken);
1819
+ const orderWId = getSubAccountId(orderWallet);
1820
+ const orderWStrategies = getAllStrategiesForWallet(allStates, orderWId, !!orderWallet.isDefault);
1821
+ const activeAsk = orderWStrategies.find((s) => s.active);
1822
+ if (activeAsk) {
1823
+ warn(`Autopilot "${strategyDisplayName(activeAsk)}" is ON for "${orderWallet.name ?? 'this wallet'}" — manual orders are disabled.`);
793
1824
  info('Turn off autopilot first: minara perps autopilot');
794
1825
  return;
795
1826
  }
@@ -806,10 +1837,10 @@ const askCmd = new Command('ask')
806
1837
  await requireTransactionConfirmation(`Perps ${isBuy ? 'LONG' : 'SHORT'} ${symbol} · size ${size} @ ~$${entryPrice.toLocaleString()}`);
807
1838
  await requireTouchId();
808
1839
  const orderSpin = spinner('Placing order…');
809
- const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na' });
1840
+ const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na', subAccountId: orderWalletId });
810
1841
  orderSpin.stop();
811
1842
  assertApiOk(orderRes, 'Order placement failed');
812
- success('Order submitted!');
1843
+ success(`Order submitted on ${getSubAccountLabel(orderWallet)}!`);
813
1844
  printTxResult(orderRes.data);
814
1845
  }));
815
1846
  /** Try to extract a tradeable recommendation from the AI analysis response. */
@@ -886,7 +1917,8 @@ function flattenObj(obj, prefix = '') {
886
1917
  // Parent
887
1918
  // ═════════════════════════════════════════════════════════════════════════
888
1919
  export const perpsCommand = new Command('perps')
889
- .description('Hyperliquid perpetual futures — order, positions, autopilot, analysis')
1920
+ .description('Hyperliquid perpetual futures — wallets, order, positions, autopilot, analysis')
1921
+ .addCommand(walletsCmd)
890
1922
  .addCommand(positionsCmd)
891
1923
  .addCommand(orderCmd)
892
1924
  .addCommand(cancelCmd)
@@ -898,6 +1930,10 @@ export const perpsCommand = new Command('perps')
898
1930
  .addCommand(fundRecordsCmd)
899
1931
  .addCommand(autopilotCmd)
900
1932
  .addCommand(askCmd)
1933
+ .addCommand(createWalletCmd)
1934
+ .addCommand(renameWalletCmd)
1935
+ .addCommand(sweepCmd)
1936
+ .addCommand(transferCmd)
901
1937
  .action(wrapAction(async () => {
902
1938
  const creds = requireAuth();
903
1939
  // Show autopilot status inline
@@ -906,6 +1942,7 @@ export const perpsCommand = new Command('perps')
906
1942
  const action = await select({
907
1943
  message: 'Perps — what would you like to do?',
908
1944
  choices: [
1945
+ { name: 'View wallets', value: 'wallets' },
909
1946
  { name: 'View positions', value: 'positions' },
910
1947
  { name: 'Place order', value: 'order' },
911
1948
  { name: 'Close position', value: 'close' },
@@ -917,6 +1954,11 @@ export const perpsCommand = new Command('perps')
917
1954
  { name: 'Fund records', value: 'fund-records' },
918
1955
  { name: `Autopilot${apLabel}`, value: 'autopilot' },
919
1956
  { name: 'Ask AI (long/short analysis)', value: 'ask' },
1957
+ { name: chalk.dim('─── Wallet Management ───'), value: '_sep', disabled: true },
1958
+ { name: 'Create sub-wallet', value: 'create-wallet' },
1959
+ { name: 'Rename sub-wallet', value: 'rename-wallet' },
1960
+ { name: 'Sweep funds → default', value: 'sweep' },
1961
+ { name: 'Transfer between wallets', value: 'transfer' },
920
1962
  ],
921
1963
  });
922
1964
  const sub = perpsCommand.commands.find((c) => c.name() === action || c.aliases().includes(action));