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.
- package/README.md +25 -8
- package/dist/api/perps.d.ts +25 -1
- package/dist/api/perps.js +50 -0
- package/dist/commands/assets.js +56 -9
- package/dist/commands/balance.js +8 -3
- package/dist/commands/perps.js +1245 -203
- package/dist/types.d.ts +41 -0
- package/package.json +2 -1
package/dist/commands/perps.js
CHANGED
|
@@ -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 →
|
|
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
|
|
73
|
-
.
|
|
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
|
-
|
|
76
|
-
|
|
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 (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log(
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
471
|
+
const allStates = await getAllAutopilotStates(creds.accessToken);
|
|
114
472
|
apSpin.stop();
|
|
115
|
-
|
|
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(
|
|
118
|
-
info(`Trading symbols: ${
|
|
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(
|
|
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
|
|
244
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
.
|
|
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
|
|
517
|
-
|
|
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
|
|
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
|
|
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 ? '+' : ''}${
|
|
537
|
-
console.log(` Total Fees : ${chalk.dim(
|
|
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
|
-
|
|
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(
|
|
564
|
-
if (
|
|
565
|
-
printTable(
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
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 (
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
??
|
|
587
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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(
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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(
|
|
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));
|