minara 0.2.4 → 0.2.6
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 +37 -24
- package/dist/api/payment.d.ts +4 -0
- package/dist/api/payment.js +5 -1
- package/dist/api/perps.d.ts +58 -0
- package/dist/api/perps.js +97 -0
- package/dist/commands/deposit.js +63 -3
- package/dist/commands/perps.js +504 -52
- package/dist/formatters.d.ts +4 -0
- package/dist/formatters.js +66 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
21
|
- **AI Chat** — Crypto-native AI for on-chain analysis, market research, and DeFi due diligence. Interactive REPL & single-shot queries with `fast` / `quality` / `thinking` modes
|
|
22
|
-
- **Wallet & Balance** — Unified balance view, spot holdings with PnL, perps account overview, deposits and
|
|
22
|
+
- **Wallet & Balance** — Unified balance view, spot holdings with PnL, perps account overview, deposits, withdrawals, and credit card on-ramp via MoonPay
|
|
23
23
|
- **Chain-Abstracted Trading** — Cross-chain swaps with automatic chain detection, perpetual futures, and limit orders. Accepts `$TICKER`, token name, or contract address
|
|
24
|
-
- **
|
|
24
|
+
- **AI Autopilot & Analysis** — Fully managed AI trading strategies for perps, plus on-demand long/short analysis with one-click quick order
|
|
25
|
+
- **Market Discovery** — Trending tokens & stocks, Fear & Greed Index, on-chain metrics, and search
|
|
25
26
|
|
|
26
27
|
## Installation
|
|
27
28
|
|
|
@@ -68,7 +69,7 @@ minara discover trending
|
|
|
68
69
|
|
|
69
70
|
| Command | Description |
|
|
70
71
|
| ---------------- | ------------------------------------------- |
|
|
71
|
-
| `minara login` | Login via device code or email
|
|
72
|
+
| `minara login` | Login via device code or email |
|
|
72
73
|
| `minara logout` | Logout and clear local credentials |
|
|
73
74
|
| `minara account` | View your account info and wallet addresses |
|
|
74
75
|
|
|
@@ -80,23 +81,25 @@ minara login -e user@mail.com # Email verification code
|
|
|
80
81
|
|
|
81
82
|
### Wallet & Funds
|
|
82
83
|
|
|
83
|
-
| Command | Description
|
|
84
|
-
| --------------------- |
|
|
85
|
-
| `minara balance` | Combined USDC/USDT balance across spot and perps
|
|
86
|
-
| `minara assets` | Full overview: spot holdings + perps account
|
|
87
|
-
| `minara assets spot` | Spot wallet: portfolio value, cost, PnL, holdings
|
|
88
|
-
| `minara assets perps` | Perps account: equity, margin, positions
|
|
89
|
-
| `minara deposit` | Deposit to spot
|
|
90
|
-
| `minara
|
|
84
|
+
| Command | Description |
|
|
85
|
+
| --------------------- | -------------------------------------------------------------- |
|
|
86
|
+
| `minara balance` | Combined USDC/USDT balance across spot and perps |
|
|
87
|
+
| `minara assets` | Full overview: spot holdings + perps account |
|
|
88
|
+
| `minara assets spot` | Spot wallet: portfolio value, cost, PnL, holdings |
|
|
89
|
+
| `minara assets perps` | Perps account: equity, margin, positions |
|
|
90
|
+
| `minara deposit` | Deposit to spot, perps, or buy crypto with credit card |
|
|
91
|
+
| `minara deposit buy` | Buy crypto with credit card via MoonPay |
|
|
92
|
+
| `minara withdraw` | Withdraw tokens to an external wallet |
|
|
91
93
|
|
|
92
94
|
```bash
|
|
93
95
|
minara balance # Quick total: Spot + Perps available balance
|
|
94
96
|
minara assets # Full overview (spot + perps)
|
|
95
97
|
minara assets spot # Spot wallet with PnL breakdown
|
|
96
98
|
minara assets perps # Perps equity, margin, positions
|
|
97
|
-
minara deposit # Interactive: Spot
|
|
99
|
+
minara deposit # Interactive: Spot / Perps / Buy with credit card
|
|
98
100
|
minara deposit spot # Show spot wallet deposit addresses (EVM + Solana)
|
|
99
101
|
minara deposit perps # Perps: show Arbitrum deposit address, or transfer from Spot → Perps
|
|
102
|
+
minara deposit buy # Buy crypto with credit card via MoonPay (opens browser)
|
|
100
103
|
minara withdraw -c solana -t '$SOL' -a 10 --to <address>
|
|
101
104
|
minara withdraw # Interactive mode (accepts ticker or address)
|
|
102
105
|
```
|
|
@@ -121,25 +124,35 @@ minara swap --dry-run # Simulate without executing
|
|
|
121
124
|
|
|
122
125
|
### Perpetual Futures
|
|
123
126
|
|
|
124
|
-
| Command | Description
|
|
125
|
-
| --------------------------- |
|
|
127
|
+
| Command | Description |
|
|
128
|
+
| --------------------------- | ----------------------------------------------------- |
|
|
129
|
+
| `minara perps positions` | View all open positions with PnL |
|
|
130
|
+
| `minara perps order` | Place an order (interactive builder) |
|
|
131
|
+
| `minara perps cancel` | Cancel open orders |
|
|
132
|
+
| `minara perps leverage` | Update leverage for a symbol |
|
|
133
|
+
| `minara perps trades` | View trade history (Hyperliquid fills) |
|
|
126
134
|
| `minara perps deposit` | Deposit USDC to perps (or use `minara deposit perps`) |
|
|
127
|
-
| `minara perps withdraw` | Withdraw USDC from perps account
|
|
128
|
-
| `minara perps
|
|
129
|
-
| `minara perps
|
|
130
|
-
| `minara perps
|
|
131
|
-
| `minara perps leverage` | Update leverage for a symbol |
|
|
132
|
-
| `minara perps trades` | View completed trade history |
|
|
133
|
-
| `minara perps fund-records` | View fund deposit/withdrawal records |
|
|
135
|
+
| `minara perps withdraw` | Withdraw USDC from perps account |
|
|
136
|
+
| `minara perps fund-records` | View fund deposit/withdrawal records |
|
|
137
|
+
| `minara perps autopilot` | Manage AI autopilot trading strategy (on/off/config) |
|
|
138
|
+
| `minara perps ask` | AI long/short analysis with quick order |
|
|
134
139
|
|
|
135
140
|
```bash
|
|
141
|
+
minara perps positions # List positions with equity, margin, PnL
|
|
142
|
+
minara perps order # Interactive: symbol selector → side → size → confirm
|
|
143
|
+
minara perps leverage # Interactive: shows max leverage per asset
|
|
144
|
+
minara perps trades # Recent fills from Hyperliquid (default 7 days)
|
|
145
|
+
minara perps trades -d 30 # Last 30 days of trade history
|
|
136
146
|
minara perps deposit -a 100 # Deposit 100 USDC to perps
|
|
137
147
|
minara perps withdraw -a 50 # Withdraw 50 USDC from perps
|
|
138
|
-
minara perps
|
|
139
|
-
minara perps
|
|
140
|
-
minara perps leverage # Interactive: set leverage for a trading pair
|
|
148
|
+
minara perps autopilot # Toggle AI autopilot, create/update strategy
|
|
149
|
+
minara perps ask # AI analysis → optional quick order
|
|
141
150
|
```
|
|
142
151
|
|
|
152
|
+
> **Autopilot:** When autopilot is ON, manual order placement (`minara perps order`) is blocked to prevent conflicts with AI-managed trades. Turn off autopilot first via `minara perps autopilot`.
|
|
153
|
+
>
|
|
154
|
+
> **Ask AI → Quick Order:** After the AI analysis, you can instantly place a market order based on the recommended direction, entry price, and position size — no need to re-enter parameters.
|
|
155
|
+
|
|
143
156
|
### Limit Orders
|
|
144
157
|
|
|
145
158
|
| Command | Description |
|
package/dist/api/payment.d.ts
CHANGED
|
@@ -15,3 +15,7 @@ export declare function checkoutPackage(token: string, packageId: string, succes
|
|
|
15
15
|
export declare function cryptoCheckoutPackage(token: string, packageId: string): Promise<import("../types.js").ApiResponse<CryptoCheckout>>;
|
|
16
16
|
/** Cancel current subscription */
|
|
17
17
|
export declare function cancelSubscription(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
18
|
+
/** Sign a MoonPay widget URL (backend appends HMAC signature). */
|
|
19
|
+
export declare function getMoonPaySignature(token: string, url: string): Promise<import("../types.js").ApiResponse<{
|
|
20
|
+
signature: string;
|
|
21
|
+
}>>;
|
package/dist/api/payment.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { get, del } from './client.js';
|
|
1
|
+
import { get, post, del } from './client.js';
|
|
2
2
|
/** Get all subscription plans and credit packages */
|
|
3
3
|
export function getPlans() {
|
|
4
4
|
return get('/payment/plans');
|
|
@@ -37,3 +37,7 @@ export function cryptoCheckoutPackage(token, packageId) {
|
|
|
37
37
|
export function cancelSubscription(token) {
|
|
38
38
|
return del('/payment/subscription', { token });
|
|
39
39
|
}
|
|
40
|
+
/** Sign a MoonPay widget URL (backend appends HMAC signature). */
|
|
41
|
+
export function getMoonPaySignature(token, url) {
|
|
42
|
+
return post('/payment/moonpay/signature', { token, body: { url } });
|
|
43
|
+
}
|
package/dist/api/perps.d.ts
CHANGED
|
@@ -27,3 +27,61 @@ export declare function getAccountSummary(token: string): Promise<import("../typ
|
|
|
27
27
|
export declare function getDecisions(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
|
|
28
28
|
/** Claim rewards */
|
|
29
29
|
export declare function claimRewards(token: string): Promise<import("../types.js").ApiResponse<TransactionResult>>;
|
|
30
|
+
export declare function getStrategies(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
|
|
31
|
+
export declare function getSupportedSymbols(token: string): Promise<import("../types.js").ApiResponse<string[]>>;
|
|
32
|
+
export declare function createStrategy(token: string, dto: {
|
|
33
|
+
symbols: string[];
|
|
34
|
+
strategyConfig?: Record<string, unknown>;
|
|
35
|
+
language?: string;
|
|
36
|
+
}): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
37
|
+
export declare function enableStrategy(token: string, strategyId: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
38
|
+
export declare function disableStrategy(token: string, strategyId: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
39
|
+
export declare function updateStrategy(token: string, dto: {
|
|
40
|
+
strategyId: string;
|
|
41
|
+
symbols: string[];
|
|
42
|
+
strategyConfig?: Record<string, unknown>;
|
|
43
|
+
language?: string;
|
|
44
|
+
}): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
45
|
+
export declare function getPerformanceMetrics(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
46
|
+
export declare function priceAnalysis(token: string, dto: {
|
|
47
|
+
symbol: string;
|
|
48
|
+
startTime?: number;
|
|
49
|
+
endTime?: number;
|
|
50
|
+
interval?: string;
|
|
51
|
+
positionUSD?: number;
|
|
52
|
+
leverage?: number;
|
|
53
|
+
}): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
54
|
+
/** Get perps wallet address from user profile */
|
|
55
|
+
export declare function getPerpsAddress(token: string): Promise<string | null>;
|
|
56
|
+
export interface HlAssetMeta {
|
|
57
|
+
name: string;
|
|
58
|
+
maxLeverage: number;
|
|
59
|
+
szDecimals: number;
|
|
60
|
+
}
|
|
61
|
+
export interface HlAssetInfo extends HlAssetMeta {
|
|
62
|
+
markPx: number;
|
|
63
|
+
}
|
|
64
|
+
/** Fetch perpetuals universe metadata + live prices from Hyperliquid (cached per session). */
|
|
65
|
+
export declare function getAssetMeta(): Promise<HlAssetInfo[]>;
|
|
66
|
+
export interface HlFill {
|
|
67
|
+
coin: string;
|
|
68
|
+
px: string;
|
|
69
|
+
sz: string;
|
|
70
|
+
side: string;
|
|
71
|
+
time: number;
|
|
72
|
+
dir: string;
|
|
73
|
+
closedPnl: string;
|
|
74
|
+
fee: string;
|
|
75
|
+
oid: number;
|
|
76
|
+
tid: number;
|
|
77
|
+
}
|
|
78
|
+
/** Fetch user trade fills directly from Hyperliquid (last 7 days by default). */
|
|
79
|
+
export declare function getUserFills(address: string, days?: number): Promise<HlFill[]>;
|
|
80
|
+
export interface HlLeverageInfo {
|
|
81
|
+
coin: string;
|
|
82
|
+
leverageType: string;
|
|
83
|
+
leverageValue: number;
|
|
84
|
+
maxLeverage: number;
|
|
85
|
+
}
|
|
86
|
+
/** Fetch user's per-asset leverage from Hyperliquid clearinghouseState. */
|
|
87
|
+
export declare function getUserLeverage(address: string): Promise<HlLeverageInfo[]>;
|
package/dist/api/perps.js
CHANGED
|
@@ -55,3 +55,100 @@ export function getDecisions(token) {
|
|
|
55
55
|
export function claimRewards(token) {
|
|
56
56
|
return post('/v1/tx/perps/claim-rewards', { token });
|
|
57
57
|
}
|
|
58
|
+
// ── Autopilot (Fully Managed Strategy) ───────────────────────────────────
|
|
59
|
+
export function getStrategies(token) {
|
|
60
|
+
return get('/v1/fully-managed/strategies', { token });
|
|
61
|
+
}
|
|
62
|
+
export function getSupportedSymbols(token) {
|
|
63
|
+
return get('/v1/fully-managed/supported-symbols', { token });
|
|
64
|
+
}
|
|
65
|
+
export function createStrategy(token, dto) {
|
|
66
|
+
return post('/v1/fully-managed/create-strategy', { token, body: dto });
|
|
67
|
+
}
|
|
68
|
+
export function enableStrategy(token, strategyId) {
|
|
69
|
+
return post('/v1/fully-managed/enable-strategy', { token, body: { strategyId } });
|
|
70
|
+
}
|
|
71
|
+
export function disableStrategy(token, strategyId) {
|
|
72
|
+
return post('/v1/fully-managed/disable-strategy', { token, body: { strategyId } });
|
|
73
|
+
}
|
|
74
|
+
export function updateStrategy(token, dto) {
|
|
75
|
+
return post('/v1/fully-managed/update-strategy', { token, body: dto });
|
|
76
|
+
}
|
|
77
|
+
export function getPerformanceMetrics(token) {
|
|
78
|
+
return get('/v1/fully-managed/performance/metrics/v2', { token });
|
|
79
|
+
}
|
|
80
|
+
// ── Price Analysis (Ask Long/Short) ──────────────────────────────────────
|
|
81
|
+
export function priceAnalysis(token, dto) {
|
|
82
|
+
return post('/tokens/price-analysis', { token, body: dto });
|
|
83
|
+
}
|
|
84
|
+
/** Get perps wallet address from user profile */
|
|
85
|
+
export async function getPerpsAddress(token) {
|
|
86
|
+
const res = await get('/auth/me', { token });
|
|
87
|
+
if (!res.success || !res.data)
|
|
88
|
+
return null;
|
|
89
|
+
return res.data.wallets?.['perpetual-evm'] ?? null;
|
|
90
|
+
}
|
|
91
|
+
let _assetInfoCache = null;
|
|
92
|
+
/** Fetch perpetuals universe metadata + live prices from Hyperliquid (cached per session). */
|
|
93
|
+
export async function getAssetMeta() {
|
|
94
|
+
if (_assetInfoCache)
|
|
95
|
+
return _assetInfoCache;
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch('https://api.hyperliquid.xyz/info', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs' }),
|
|
101
|
+
});
|
|
102
|
+
const json = (await res.json());
|
|
103
|
+
const [meta, ctxs] = json;
|
|
104
|
+
_assetInfoCache = (meta.universe ?? []).map((m, i) => ({
|
|
105
|
+
...m,
|
|
106
|
+
markPx: Number(ctxs?.[i]?.markPx ?? 0),
|
|
107
|
+
}));
|
|
108
|
+
return _assetInfoCache;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Fetch user trade fills directly from Hyperliquid (last 7 days by default). */
|
|
115
|
+
export async function getUserFills(address, days = 7) {
|
|
116
|
+
try {
|
|
117
|
+
const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
118
|
+
const res = await fetch('https://api.hyperliquid.xyz/info', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
type: 'userFillsByTime',
|
|
123
|
+
user: address,
|
|
124
|
+
startTime,
|
|
125
|
+
aggregateByTime: true,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
const data = await res.json();
|
|
129
|
+
return Array.isArray(data) ? data : [];
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Fetch user's per-asset leverage from Hyperliquid clearinghouseState. */
|
|
136
|
+
export async function getUserLeverage(address) {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch('https://api.hyperliquid.xyz/info', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({ type: 'clearinghouseState', user: address }),
|
|
142
|
+
});
|
|
143
|
+
const data = (await res.json());
|
|
144
|
+
return (data.assetPositions ?? []).map((ap) => ({
|
|
145
|
+
coin: ap.position.coin,
|
|
146
|
+
leverageType: ap.position.leverage.type,
|
|
147
|
+
leverageValue: ap.position.leverage.value,
|
|
148
|
+
maxLeverage: ap.position.maxLeverage,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
package/dist/commands/deposit.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getAccount } from '../api/crosschain.js';
|
|
|
5
5
|
import { getCurrentUser } from '../api/auth.js';
|
|
6
6
|
import * as perpsApi from '../api/perps.js';
|
|
7
7
|
import { requireAuth } from '../config.js';
|
|
8
|
-
import { info, success, spinner, assertApiOk, wrapAction, requireTransactionConfirmation } from '../utils.js';
|
|
8
|
+
import { info, success, warn, spinner, assertApiOk, wrapAction, requireTransactionConfirmation, openBrowser } from '../utils.js';
|
|
9
9
|
import { requireTouchId } from '../touchid.js';
|
|
10
10
|
import { printTxResult } from '../formatters.js';
|
|
11
11
|
const EVM_CHAINS = 'Ethereum, Base, Arbitrum, Optimism, Polygon, Avalanche, BSC, Berachain, Blast';
|
|
@@ -52,6 +52,55 @@ async function showSpotDeposit(token) {
|
|
|
52
52
|
console.log(chalk.red(' • Sending tokens on the wrong network may result in permanent loss.'));
|
|
53
53
|
console.log('');
|
|
54
54
|
}
|
|
55
|
+
// ─── moonpay (credit card on-ramp) ───────────────────────────────────────
|
|
56
|
+
const MOONPAY_PK = 'pk_live_yIf64w79W6ufwip4j51PWbymdwGtI';
|
|
57
|
+
const MOONPAY_CURRENCIES = [
|
|
58
|
+
{ name: 'USDC (Base)', code: 'usdc_base', network: 'base' },
|
|
59
|
+
{ name: 'USDC (Ethereum)', code: 'usdc', network: 'ethereum' },
|
|
60
|
+
{ name: 'USDC (Arbitrum)', code: 'usdc_arbitrum', network: 'arbitrum' },
|
|
61
|
+
{ name: 'USDC (Polygon)', code: 'usdc_polygon', network: 'polygon' },
|
|
62
|
+
{ name: 'ETH', code: 'eth', network: 'ethereum' },
|
|
63
|
+
{ name: 'ETH (Base)', code: 'eth_base', network: 'base' },
|
|
64
|
+
{ name: 'SOL', code: 'sol', network: 'solana' },
|
|
65
|
+
];
|
|
66
|
+
async function moonPayOnRamp(token) {
|
|
67
|
+
const addrSpin = spinner('Fetching wallet address…');
|
|
68
|
+
const accountRes = await getAccount(token);
|
|
69
|
+
addrSpin.stop();
|
|
70
|
+
const account = accountRes.data;
|
|
71
|
+
const evmAddr = account?.evmAddress;
|
|
72
|
+
const solAddr = account?.solanaAddress;
|
|
73
|
+
if (!evmAddr && !solAddr) {
|
|
74
|
+
warn('No wallet address found. Your account may not be fully initialized.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const currency = await select({
|
|
78
|
+
message: 'Currency to buy:',
|
|
79
|
+
choices: MOONPAY_CURRENCIES
|
|
80
|
+
.filter((c) => c.network === 'solana' ? !!solAddr : !!evmAddr)
|
|
81
|
+
.map((c) => ({ name: c.name, value: c })),
|
|
82
|
+
});
|
|
83
|
+
const walletAddress = currency.network === 'solana' ? solAddr : evmAddr;
|
|
84
|
+
// Build MoonPay buy URL (no walletAddress in URL — requires server-side signing
|
|
85
|
+
// which depends on backend MoonPay secret key config).
|
|
86
|
+
// User pastes their address in MoonPay's form instead.
|
|
87
|
+
const params = new URLSearchParams();
|
|
88
|
+
params.set('apiKey', MOONPAY_PK);
|
|
89
|
+
params.set('currencyCode', currency.code);
|
|
90
|
+
params.set('defaultCurrencyCode', currency.code);
|
|
91
|
+
const buyUrl = `https://buy.moonpay.com?${params.toString()}`;
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.bold('Buy Crypto with Credit Card (MoonPay)'));
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(` Currency : ${chalk.cyan(currency.name)}`);
|
|
96
|
+
console.log(` Wallet : ${chalk.yellow(walletAddress)}`);
|
|
97
|
+
console.log(chalk.dim(' ↑ Copy this address and paste it in MoonPay when prompted.'));
|
|
98
|
+
console.log('');
|
|
99
|
+
info('Opening MoonPay in your browser…');
|
|
100
|
+
openBrowser(buyUrl);
|
|
101
|
+
console.log(chalk.dim(' Complete the purchase in your browser. Funds will arrive in your Minara wallet.'));
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
55
104
|
// ─── perps ───────────────────────────────────────────────────────────────
|
|
56
105
|
const perpsCmd = new Command('perps')
|
|
57
106
|
.description('Deposit USDC to perps account')
|
|
@@ -128,23 +177,34 @@ async function transferSpotToPerps(token, opts) {
|
|
|
128
177
|
printTxResult(res.data);
|
|
129
178
|
}
|
|
130
179
|
// ─── parent ──────────────────────────────────────────────────────────────
|
|
180
|
+
const buyCmd = new Command('buy')
|
|
181
|
+
.description('Buy crypto with credit card via MoonPay')
|
|
182
|
+
.action(wrapAction(async () => {
|
|
183
|
+
const creds = requireAuth();
|
|
184
|
+
await moonPayOnRamp(creds.accessToken);
|
|
185
|
+
}));
|
|
131
186
|
export const depositCommand = new Command('deposit')
|
|
132
|
-
.description('Deposit to spot wallet or perps account')
|
|
187
|
+
.description('Deposit to spot wallet or perps account, or buy with credit card')
|
|
133
188
|
.addCommand(spotCmd)
|
|
134
189
|
.addCommand(perpsCmd)
|
|
190
|
+
.addCommand(buyCmd)
|
|
135
191
|
.action(wrapAction(async () => {
|
|
136
192
|
const action = await select({
|
|
137
193
|
message: 'Deposit to:',
|
|
138
194
|
choices: [
|
|
139
195
|
{ name: 'Spot wallet — view deposit addresses', value: 'spot' },
|
|
140
196
|
{ name: 'Perps wallet — view deposit address or transfer from Spot', value: 'perps' },
|
|
197
|
+
{ name: `Buy crypto with credit card ${chalk.dim('(MoonPay)')}`, value: 'buy' },
|
|
141
198
|
],
|
|
142
199
|
});
|
|
143
200
|
const creds = requireAuth();
|
|
144
201
|
if (action === 'spot') {
|
|
145
202
|
await showSpotDeposit(creds.accessToken);
|
|
146
203
|
}
|
|
147
|
-
else {
|
|
204
|
+
else if (action === 'perps') {
|
|
148
205
|
await perpsDepositFlow(creds.accessToken);
|
|
149
206
|
}
|
|
207
|
+
else {
|
|
208
|
+
await moonPayOnRamp(creds.accessToken);
|
|
209
|
+
}
|
|
150
210
|
}));
|
package/dist/commands/perps.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as perpsApi from '../api/perps.js';
|
|
|
5
5
|
import { requireAuth } from '../config.js';
|
|
6
6
|
import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation } from '../utils.js';
|
|
7
7
|
import { requireTouchId } from '../touchid.js';
|
|
8
|
-
import { printTxResult, printTable, POSITION_COLUMNS } from '../formatters.js';
|
|
8
|
+
import { printTxResult, printTable, printKV, POSITION_COLUMNS, FILL_COLUMNS } from '../formatters.js';
|
|
9
9
|
// ─── deposit ─────────────────────────────────────────────────────────────
|
|
10
10
|
const depositCmd = new Command('deposit')
|
|
11
11
|
.description('Deposit USDC into Hyperliquid perps (min 5 USDC)')
|
|
@@ -73,19 +73,34 @@ const positionsCmd = new Command('positions')
|
|
|
73
73
|
.action(wrapAction(async () => {
|
|
74
74
|
const creds = requireAuth();
|
|
75
75
|
const spin = spinner('Fetching positions…');
|
|
76
|
-
const res = await perpsApi.
|
|
76
|
+
const res = await perpsApi.getAccountSummary(creds.accessToken);
|
|
77
77
|
spin.stop();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
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.'));
|
|
82
99
|
}
|
|
83
100
|
else {
|
|
84
|
-
console.log('');
|
|
85
|
-
console.log(chalk.bold('Open Positions:'));
|
|
86
101
|
printTable(positions, POSITION_COLUMNS);
|
|
87
|
-
console.log('');
|
|
88
102
|
}
|
|
103
|
+
console.log('');
|
|
89
104
|
}));
|
|
90
105
|
// ─── order ───────────────────────────────────────────────────────────────
|
|
91
106
|
const orderCmd = new Command('order')
|
|
@@ -93,19 +108,29 @@ const orderCmd = new Command('order')
|
|
|
93
108
|
.option('-y, --yes', 'Skip confirmation')
|
|
94
109
|
.action(wrapAction(async (opts) => {
|
|
95
110
|
const creds = requireAuth();
|
|
111
|
+
// Check autopilot — block manual orders while AI is trading
|
|
112
|
+
const apSpin = spinner('Checking autopilot…');
|
|
113
|
+
const apState = await getAutopilotState(creds.accessToken);
|
|
114
|
+
apSpin.stop();
|
|
115
|
+
if (apState.active) {
|
|
116
|
+
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'}`);
|
|
119
|
+
info('Turn off autopilot first: minara perps autopilot');
|
|
120
|
+
console.log('');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
96
123
|
info('Building a Hyperliquid perps order…');
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
if (prices.length > 10)
|
|
108
|
-
console.log(chalk.dim(` … and ${prices.length - 10} more`));
|
|
124
|
+
const dataSpin = spinner('Fetching market data…');
|
|
125
|
+
const address = await perpsApi.getPerpsAddress(creds.accessToken);
|
|
126
|
+
const [assets, leverages] = await Promise.all([
|
|
127
|
+
perpsApi.getAssetMeta(),
|
|
128
|
+
address ? perpsApi.getUserLeverage(address) : Promise.resolve([]),
|
|
129
|
+
]);
|
|
130
|
+
dataSpin.stop();
|
|
131
|
+
const leverageMap = new Map();
|
|
132
|
+
for (const l of leverages) {
|
|
133
|
+
leverageMap.set(l.coin.toUpperCase(), { value: l.leverageValue, type: l.leverageType });
|
|
109
134
|
}
|
|
110
135
|
const isBuy = await select({
|
|
111
136
|
message: 'Side:',
|
|
@@ -114,15 +139,57 @@ const orderCmd = new Command('order')
|
|
|
114
139
|
{ name: 'Short (sell)', value: false },
|
|
115
140
|
],
|
|
116
141
|
});
|
|
117
|
-
|
|
142
|
+
let asset;
|
|
143
|
+
if (assets.length > 0) {
|
|
144
|
+
asset = await select({
|
|
145
|
+
message: 'Asset:',
|
|
146
|
+
choices: assets.map((a) => {
|
|
147
|
+
const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
|
|
148
|
+
const lev = leverageMap.get(a.name.toUpperCase());
|
|
149
|
+
const levStr = lev ? `${lev.value}x ${lev.type}` : '';
|
|
150
|
+
return {
|
|
151
|
+
name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))} ${chalk.dim(`max ${a.maxLeverage}x`)}${levStr ? ` ${chalk.cyan(levStr)}` : ''}`,
|
|
152
|
+
value: a.name,
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
asset = await input({ message: 'Asset symbol (e.g. BTC, ETH):' });
|
|
159
|
+
}
|
|
160
|
+
const currentLev = leverageMap.get(asset.toUpperCase());
|
|
161
|
+
if (currentLev) {
|
|
162
|
+
info(`Current leverage: ${currentLev.value}x (${currentLev.type})`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
info(`No leverage set for ${asset} — use 'minara perps leverage' to configure`);
|
|
166
|
+
}
|
|
118
167
|
const orderType = await select({
|
|
119
168
|
message: 'Order type:',
|
|
120
169
|
choices: [
|
|
170
|
+
{ name: 'Market', value: 'market' },
|
|
121
171
|
{ name: 'Limit', value: 'limit' },
|
|
122
|
-
{ name: 'Market (trigger)', value: 'market' },
|
|
123
172
|
],
|
|
124
173
|
});
|
|
125
|
-
const
|
|
174
|
+
const assetMeta = assets.find((a) => a.name.toUpperCase() === asset.toUpperCase());
|
|
175
|
+
let limitPx;
|
|
176
|
+
let marketPx;
|
|
177
|
+
if (orderType === 'limit') {
|
|
178
|
+
limitPx = await input({ message: 'Limit price:' });
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
marketPx = assetMeta?.markPx;
|
|
182
|
+
if (marketPx && marketPx > 0) {
|
|
183
|
+
const slippagePx = isBuy ? marketPx * 1.01 : marketPx * 0.99;
|
|
184
|
+
limitPx = slippagePx.toPrecision(6);
|
|
185
|
+
info(`Market order at ~$${marketPx}`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
warn(`Could not fetch current price for ${asset}. Enter the approximate market price.`);
|
|
189
|
+
limitPx = await input({ message: 'Price:' });
|
|
190
|
+
marketPx = Number(limitPx);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
126
193
|
const sz = await input({ message: 'Size (in contracts):' });
|
|
127
194
|
const reduceOnly = await confirm({ message: 'Reduce only?', default: false });
|
|
128
195
|
const grouping = await select({
|
|
@@ -141,26 +208,24 @@ const orderCmd = new Command('order')
|
|
|
141
208
|
r: reduceOnly,
|
|
142
209
|
t: orderType === 'limit'
|
|
143
210
|
? { limit: { tif: 'Gtc' } }
|
|
144
|
-
: { trigger: { triggerPx: limitPx, tpsl: 'tp', isMarket: true } },
|
|
211
|
+
: { trigger: { triggerPx: String(marketPx ?? limitPx), tpsl: 'tp', isMarket: true } },
|
|
145
212
|
};
|
|
213
|
+
const priceLabel = orderType === 'market' ? `Market (~$${marketPx ?? limitPx})` : `$${limitPx}`;
|
|
214
|
+
const levLabel = currentLev ? `${currentLev.value}x (${currentLev.type})` : '—';
|
|
146
215
|
console.log('');
|
|
147
216
|
console.log(chalk.bold('Order Preview:'));
|
|
148
217
|
console.log(` Asset : ${chalk.bold(order.a)}`);
|
|
149
218
|
console.log(` Side : ${formatOrderSide(order.b ? 'buy' : 'sell')}`);
|
|
150
|
-
console.log(`
|
|
219
|
+
console.log(` Leverage : ${chalk.cyan(levLabel)}`);
|
|
220
|
+
console.log(` Type : ${orderType === 'market' ? 'Market' : 'Limit (GTC)'}`);
|
|
221
|
+
console.log(` Price : ${chalk.yellow(priceLabel)}`);
|
|
151
222
|
console.log(` Size : ${chalk.bold(order.s)}`);
|
|
152
223
|
console.log(` Reduce Only : ${order.r ? chalk.yellow('Yes') : 'No'}`);
|
|
153
|
-
console.log(` Type : ${'limit' in order.t ? 'Limit (GTC)' : 'Trigger'}`);
|
|
154
224
|
console.log(` Grouping : ${grouping}`);
|
|
155
225
|
console.log('');
|
|
156
226
|
if (!opts.yes) {
|
|
157
|
-
|
|
158
|
-
if (!ok) {
|
|
159
|
-
console.log(chalk.dim('Cancelled.'));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
227
|
+
await requireTransactionConfirmation(`Perps ${order.b ? 'LONG' : 'SHORT'} ${order.a} · size ${order.s} @ ${priceLabel}`);
|
|
162
228
|
}
|
|
163
|
-
await requireTransactionConfirmation(`Perps ${order.b ? 'LONG' : 'SHORT'} ${order.a} · size ${order.s} @ ${order.p}`);
|
|
164
229
|
await requireTouchId();
|
|
165
230
|
const spin = spinner('Placing order…');
|
|
166
231
|
const res = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping });
|
|
@@ -175,7 +240,25 @@ const cancelCmd = new Command('cancel')
|
|
|
175
240
|
.option('-y, --yes', 'Skip confirmation')
|
|
176
241
|
.action(wrapAction(async (opts) => {
|
|
177
242
|
const creds = requireAuth();
|
|
178
|
-
const
|
|
243
|
+
const metaSpin = spinner('Fetching assets…');
|
|
244
|
+
const assets = await perpsApi.getAssetMeta();
|
|
245
|
+
metaSpin.stop();
|
|
246
|
+
let asset;
|
|
247
|
+
if (assets.length > 0) {
|
|
248
|
+
asset = await select({
|
|
249
|
+
message: 'Asset to cancel:',
|
|
250
|
+
choices: assets.map((a) => {
|
|
251
|
+
const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
|
|
252
|
+
return {
|
|
253
|
+
name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))} ${chalk.dim(`max ${a.maxLeverage}x`)}`,
|
|
254
|
+
value: a.name,
|
|
255
|
+
};
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
asset = await input({ message: 'Asset symbol to cancel (e.g. BTC):' });
|
|
261
|
+
}
|
|
179
262
|
const oid = await input({
|
|
180
263
|
message: 'Order ID (oid):',
|
|
181
264
|
validate: (v) => {
|
|
@@ -200,8 +283,33 @@ const leverageCmd = new Command('leverage')
|
|
|
200
283
|
.description('Update leverage for a symbol')
|
|
201
284
|
.action(wrapAction(async () => {
|
|
202
285
|
const creds = requireAuth();
|
|
203
|
-
const
|
|
204
|
-
const
|
|
286
|
+
const metaSpin = spinner('Fetching available assets…');
|
|
287
|
+
const assets = await perpsApi.getAssetMeta();
|
|
288
|
+
metaSpin.stop();
|
|
289
|
+
let symbol;
|
|
290
|
+
if (assets.length > 0) {
|
|
291
|
+
symbol = await select({
|
|
292
|
+
message: 'Asset:',
|
|
293
|
+
choices: assets.map((a) => {
|
|
294
|
+
const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
|
|
295
|
+
return {
|
|
296
|
+
name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))} ${chalk.dim(`max ${a.maxLeverage}x`)}`,
|
|
297
|
+
value: a.name,
|
|
298
|
+
};
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
symbol = await input({ message: 'Symbol (e.g. BTC):' });
|
|
304
|
+
}
|
|
305
|
+
const meta = assets.find((a) => a.name.toUpperCase() === symbol.toUpperCase());
|
|
306
|
+
const maxLev = meta?.maxLeverage ?? 50;
|
|
307
|
+
const leverage = await numberPrompt({
|
|
308
|
+
message: `Leverage (1–${maxLev}x):`,
|
|
309
|
+
min: 1,
|
|
310
|
+
max: maxLev,
|
|
311
|
+
required: true,
|
|
312
|
+
});
|
|
205
313
|
const isCross = await select({
|
|
206
314
|
message: 'Margin mode:',
|
|
207
315
|
choices: [
|
|
@@ -217,20 +325,43 @@ const leverageCmd = new Command('leverage')
|
|
|
217
325
|
}));
|
|
218
326
|
// ─── trades ──────────────────────────────────────────────────────────────
|
|
219
327
|
const tradesCmd = new Command('trades')
|
|
220
|
-
.description('View
|
|
221
|
-
.
|
|
328
|
+
.description('View your perps trade fills')
|
|
329
|
+
.option('-n, --count <n>', 'Number of recent fills to show', '20')
|
|
330
|
+
.option('-d, --days <n>', 'Look back N days', '7')
|
|
331
|
+
.action(wrapAction(async (opts) => {
|
|
222
332
|
const creds = requireAuth();
|
|
223
|
-
const spin = spinner('Fetching
|
|
224
|
-
const
|
|
333
|
+
const spin = spinner('Fetching trade history…');
|
|
334
|
+
const address = await perpsApi.getPerpsAddress(creds.accessToken);
|
|
335
|
+
if (!address) {
|
|
336
|
+
spin.stop();
|
|
337
|
+
warn('Could not find your perps wallet address. Make sure your perps account is initialized.');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const days = Math.max(1, parseInt(opts.days, 10) || 7);
|
|
341
|
+
const fills = await perpsApi.getUserFills(address, days);
|
|
225
342
|
spin.stop();
|
|
226
|
-
|
|
343
|
+
const limit = Math.max(1, parseInt(opts.count, 10) || 20);
|
|
344
|
+
const recent = fills.slice(0, limit);
|
|
345
|
+
const totalPnl = fills.reduce((s, f) => s + Number(f.closedPnl ?? 0), 0);
|
|
346
|
+
const totalFees = fills.reduce((s, f) => s + Number(f.fee ?? 0), 0);
|
|
347
|
+
const closingFills = fills.filter((f) => Number(f.closedPnl ?? 0) !== 0);
|
|
348
|
+
const wins = closingFills.filter((f) => Number(f.closedPnl) > 0).length;
|
|
349
|
+
const pnlColor = totalPnl >= 0 ? chalk.green : chalk.red;
|
|
350
|
+
const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
227
351
|
console.log('');
|
|
228
|
-
console.log(chalk.bold(
|
|
229
|
-
|
|
230
|
-
|
|
352
|
+
console.log(chalk.bold(`Trade Fills (last ${days}d — ${fills.length} fills):`));
|
|
353
|
+
console.log(` Realized PnL : ${pnlColor(`${totalPnl >= 0 ? '+' : ''}${fmt(totalPnl)}`)}`);
|
|
354
|
+
console.log(` Total Fees : ${chalk.dim(fmt(totalFees))}`);
|
|
355
|
+
if (closingFills.length > 0) {
|
|
356
|
+
console.log(` Win Rate : ${wins}/${closingFills.length} (${((wins / closingFills.length) * 100).toFixed(1)}%)`);
|
|
357
|
+
}
|
|
358
|
+
console.log('');
|
|
359
|
+
if (recent.length > 0) {
|
|
360
|
+
console.log(chalk.dim(`Showing ${recent.length} most recent:`));
|
|
361
|
+
printTable(recent, FILL_COLUMNS);
|
|
231
362
|
}
|
|
232
363
|
else {
|
|
233
|
-
console.log(chalk.dim(' No
|
|
364
|
+
console.log(chalk.dim(' No trade fills in this period.'));
|
|
234
365
|
}
|
|
235
366
|
console.log('');
|
|
236
367
|
}));
|
|
@@ -255,31 +386,352 @@ const fundRecordsCmd = new Command('fund-records')
|
|
|
255
386
|
}
|
|
256
387
|
console.log('');
|
|
257
388
|
}));
|
|
389
|
+
async function getAutopilotState(token) {
|
|
390
|
+
const res = await perpsApi.getStrategies(token);
|
|
391
|
+
if (!res.success || !res.data)
|
|
392
|
+
return { active: false };
|
|
393
|
+
// Response may be an array or { data: [...] } or nested object
|
|
394
|
+
let strategies = [];
|
|
395
|
+
const raw = res.data;
|
|
396
|
+
if (Array.isArray(raw)) {
|
|
397
|
+
strategies = raw;
|
|
398
|
+
}
|
|
399
|
+
else if (raw && typeof raw === 'object') {
|
|
400
|
+
// Might be wrapped in { strategies: [...] } or { data: [...] }
|
|
401
|
+
const inner = raw.strategies
|
|
402
|
+
?? raw.data
|
|
403
|
+
?? raw;
|
|
404
|
+
if (Array.isArray(inner)) {
|
|
405
|
+
strategies = inner;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
for (const v of Object.values(raw)) {
|
|
409
|
+
if (Array.isArray(v)) {
|
|
410
|
+
strategies.push(...v);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (strategies.length === 0)
|
|
417
|
+
return { active: false };
|
|
418
|
+
const s = strategies[0];
|
|
419
|
+
// Check all possible status field names
|
|
420
|
+
const status = String(s.status ?? s.state ?? s.isActive ?? s.enabled ?? '').toLowerCase();
|
|
421
|
+
const isActive = status === 'active' || status === 'enabled' || status === 'running'
|
|
422
|
+
|| status === 'true' || s.isActive === true || s.enabled === true;
|
|
423
|
+
return {
|
|
424
|
+
active: isActive,
|
|
425
|
+
strategyId: String(s._id ?? s.id ?? s.strategyId ?? ''),
|
|
426
|
+
symbols: Array.isArray(s.symbols) ? s.symbols : [],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
// ─── autopilot ──────────────────────────────────────────────────────────
|
|
430
|
+
const autopilotCmd = new Command('autopilot')
|
|
431
|
+
.alias('ap')
|
|
432
|
+
.description('Manage AI autopilot trading strategy')
|
|
433
|
+
.action(wrapAction(async () => {
|
|
434
|
+
const creds = requireAuth();
|
|
435
|
+
const statusSpin = spinner('Checking autopilot status…');
|
|
436
|
+
const state = await getAutopilotState(creds.accessToken);
|
|
437
|
+
statusSpin.stop();
|
|
438
|
+
const statusLabel = state.active ? chalk.green.bold('ON') : chalk.dim('OFF');
|
|
439
|
+
console.log('');
|
|
440
|
+
console.log(chalk.bold('Autopilot Status:') + ` ${statusLabel}`);
|
|
441
|
+
if (state.symbols && state.symbols.length > 0) {
|
|
442
|
+
console.log(` Symbols : ${state.symbols.join(', ')}`);
|
|
443
|
+
}
|
|
444
|
+
console.log('');
|
|
445
|
+
const action = await select({
|
|
446
|
+
message: 'What would you like to do?',
|
|
447
|
+
choices: [
|
|
448
|
+
...(state.active
|
|
449
|
+
? [{ name: chalk.red('Turn OFF autopilot'), value: 'off' }]
|
|
450
|
+
: [{ name: chalk.green('Turn ON autopilot'), value: 'on' }]),
|
|
451
|
+
...(!state.strategyId ? [{ name: 'Create autopilot strategy', value: 'create' }] : []),
|
|
452
|
+
...(state.strategyId ? [{ name: 'Update symbols', value: 'update' }] : []),
|
|
453
|
+
{ name: 'View performance', value: 'perf' },
|
|
454
|
+
{ name: 'Back', value: 'back' },
|
|
455
|
+
],
|
|
456
|
+
});
|
|
457
|
+
if (action === 'back')
|
|
458
|
+
return;
|
|
459
|
+
if (action === 'on' && state.strategyId) {
|
|
460
|
+
const spin = spinner('Enabling autopilot…');
|
|
461
|
+
const res = await perpsApi.enableStrategy(creds.accessToken, state.strategyId);
|
|
462
|
+
spin.stop();
|
|
463
|
+
assertApiOk(res, 'Failed to enable autopilot');
|
|
464
|
+
success('Autopilot is now ON');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (action === 'off' && state.strategyId) {
|
|
468
|
+
const ok = await confirm({ message: 'Turn off autopilot? AI will stop trading.', default: false });
|
|
469
|
+
if (!ok)
|
|
470
|
+
return;
|
|
471
|
+
const spin = spinner('Disabling autopilot…');
|
|
472
|
+
const res = await perpsApi.disableStrategy(creds.accessToken, state.strategyId);
|
|
473
|
+
spin.stop();
|
|
474
|
+
assertApiOk(res, 'Failed to disable autopilot');
|
|
475
|
+
success('Autopilot is now OFF');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (action === 'create' || (action === 'on' && !state.strategyId)) {
|
|
479
|
+
const symSpin = spinner('Fetching supported symbols…');
|
|
480
|
+
const symRes = await perpsApi.getSupportedSymbols(creds.accessToken);
|
|
481
|
+
symSpin.stop();
|
|
482
|
+
const supported = symRes.success && Array.isArray(symRes.data) ? symRes.data : ['BTC', 'ETH', 'SOL'];
|
|
483
|
+
info(`Supported symbols: ${supported.join(', ')}`);
|
|
484
|
+
const symbolsInput = await input({
|
|
485
|
+
message: 'Symbols to trade (comma-separated):',
|
|
486
|
+
default: supported.slice(0, 3).join(','),
|
|
487
|
+
});
|
|
488
|
+
const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
489
|
+
const spin = spinner('Creating autopilot strategy…');
|
|
490
|
+
const res = await perpsApi.createStrategy(creds.accessToken, { symbols });
|
|
491
|
+
spin.stop();
|
|
492
|
+
assertApiOk(res, 'Failed to create autopilot strategy');
|
|
493
|
+
success(`Autopilot created for ${symbols.join(', ')} and enabled!`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (action === 'update' && state.strategyId) {
|
|
497
|
+
const symSpin = spinner('Fetching supported symbols…');
|
|
498
|
+
const symRes = await perpsApi.getSupportedSymbols(creds.accessToken);
|
|
499
|
+
symSpin.stop();
|
|
500
|
+
const supported = symRes.success && Array.isArray(symRes.data) ? symRes.data : ['BTC', 'ETH', 'SOL'];
|
|
501
|
+
info(`Supported: ${supported.join(', ')} | Current: ${state.symbols?.join(', ') ?? 'none'}`);
|
|
502
|
+
const symbolsInput = await input({
|
|
503
|
+
message: 'New symbols (comma-separated):',
|
|
504
|
+
default: state.symbols?.join(',') ?? '',
|
|
505
|
+
});
|
|
506
|
+
const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
507
|
+
const spin = spinner('Updating strategy…');
|
|
508
|
+
const res = await perpsApi.updateStrategy(creds.accessToken, { strategyId: state.strategyId, symbols });
|
|
509
|
+
spin.stop();
|
|
510
|
+
assertApiOk(res, 'Failed to update strategy');
|
|
511
|
+
success(`Autopilot updated: ${symbols.join(', ')}`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (action === 'perf') {
|
|
515
|
+
const spin = spinner('Fetching performance…');
|
|
516
|
+
const res = await perpsApi.getPerformanceMetrics(creds.accessToken);
|
|
517
|
+
spin.stop();
|
|
518
|
+
if (res.success && res.data) {
|
|
519
|
+
console.log('');
|
|
520
|
+
console.log(chalk.bold('Autopilot Performance:'));
|
|
521
|
+
printKV(res.data);
|
|
522
|
+
console.log('');
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
console.log(chalk.dim(' No performance data available.'));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}));
|
|
529
|
+
// ─── ask (long/short analysis) ──────────────────────────────────────────
|
|
530
|
+
const askCmd = new Command('ask')
|
|
531
|
+
.description('Get AI trading analysis for an asset (long/short recommendation)')
|
|
532
|
+
.action(wrapAction(async () => {
|
|
533
|
+
const creds = requireAuth();
|
|
534
|
+
const dataSpin = spinner('Fetching assets…');
|
|
535
|
+
const assets = await perpsApi.getAssetMeta();
|
|
536
|
+
dataSpin.stop();
|
|
537
|
+
let symbol;
|
|
538
|
+
if (assets.length > 0) {
|
|
539
|
+
symbol = await select({
|
|
540
|
+
message: 'Asset to analyze:',
|
|
541
|
+
choices: assets.map((a) => {
|
|
542
|
+
const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
|
|
543
|
+
return { name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))}`, value: a.name };
|
|
544
|
+
}),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
symbol = await input({ message: 'Symbol (e.g. BTC):' });
|
|
549
|
+
}
|
|
550
|
+
const style = await select({
|
|
551
|
+
message: 'Analysis style:',
|
|
552
|
+
choices: [
|
|
553
|
+
{ name: 'Scalping (minutes–hours)', value: 'scalping' },
|
|
554
|
+
{ name: 'Day Trading (hours–day)', value: 'day-trading' },
|
|
555
|
+
{ name: 'Swing Trading (days–weeks)', value: 'swing-trading' },
|
|
556
|
+
],
|
|
557
|
+
});
|
|
558
|
+
const styleConfig = {
|
|
559
|
+
'scalping': { interval: '5m', hours: 4 },
|
|
560
|
+
'day-trading': { interval: '1h', hours: 24 },
|
|
561
|
+
'swing-trading': { interval: '4h', hours: 24 * 7 },
|
|
562
|
+
};
|
|
563
|
+
const { interval, hours } = styleConfig[style] ?? styleConfig['day-trading'];
|
|
564
|
+
const endTime = Date.now();
|
|
565
|
+
const startTime = endTime - hours * 60 * 60 * 1000;
|
|
566
|
+
const marginInput = await input({ message: 'Margin in USD:', default: '1000' });
|
|
567
|
+
const leverageInput = await input({ message: 'Leverage:', default: '10' });
|
|
568
|
+
const spin = spinner(`Analyzing ${symbol}…`);
|
|
569
|
+
const res = await perpsApi.priceAnalysis(creds.accessToken, {
|
|
570
|
+
symbol,
|
|
571
|
+
startTime,
|
|
572
|
+
endTime,
|
|
573
|
+
interval,
|
|
574
|
+
positionUSD: Number(marginInput),
|
|
575
|
+
leverage: Number(leverageInput),
|
|
576
|
+
});
|
|
577
|
+
spin.stop();
|
|
578
|
+
if (!res.success || !res.data) {
|
|
579
|
+
warn(res.error?.message ?? 'Analysis failed. Try again later.');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const data = res.data;
|
|
583
|
+
console.log('');
|
|
584
|
+
console.log(chalk.bold(`AI Analysis — ${symbol} (${style}):`));
|
|
585
|
+
console.log('');
|
|
586
|
+
if (typeof data === 'string') {
|
|
587
|
+
console.log(data);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
printKV(data);
|
|
591
|
+
}
|
|
592
|
+
console.log('');
|
|
593
|
+
// ── Quick Order ──────────────────────────────────────────────────
|
|
594
|
+
// Extract recommendation from the AI response
|
|
595
|
+
const recommendation = extractRecommendation(data, symbol, Number(marginInput), Number(leverageInput));
|
|
596
|
+
if (!recommendation)
|
|
597
|
+
return;
|
|
598
|
+
const { side, entryPrice, size } = recommendation;
|
|
599
|
+
const sideLabel = side === 'buy' ? chalk.green.bold('LONG') : chalk.red.bold('SHORT');
|
|
600
|
+
console.log(chalk.bold('Quick Order:'));
|
|
601
|
+
console.log(` ${sideLabel} ${chalk.bold(symbol)} | Entry ~$${entryPrice.toLocaleString()} | Size ${size} | ${Number(leverageInput)}x`);
|
|
602
|
+
console.log('');
|
|
603
|
+
const doQuick = await confirm({ message: 'Place this order now?', default: false });
|
|
604
|
+
if (!doQuick)
|
|
605
|
+
return;
|
|
606
|
+
// Check autopilot before placing
|
|
607
|
+
const apState = await getAutopilotState(creds.accessToken);
|
|
608
|
+
if (apState.active) {
|
|
609
|
+
warn('Autopilot is ON — manual orders are disabled while AI is trading.');
|
|
610
|
+
info('Turn off autopilot first: minara perps autopilot');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const isBuy = side === 'buy';
|
|
614
|
+
const slippagePx = isBuy ? entryPrice * 1.01 : entryPrice * 0.99;
|
|
615
|
+
const order = {
|
|
616
|
+
a: symbol,
|
|
617
|
+
b: isBuy,
|
|
618
|
+
p: slippagePx.toPrecision(6),
|
|
619
|
+
s: String(size),
|
|
620
|
+
r: false,
|
|
621
|
+
t: { trigger: { triggerPx: String(entryPrice), tpsl: 'tp', isMarket: true } },
|
|
622
|
+
};
|
|
623
|
+
await requireTransactionConfirmation(`Perps ${isBuy ? 'LONG' : 'SHORT'} ${symbol} · size ${size} @ ~$${entryPrice.toLocaleString()}`);
|
|
624
|
+
await requireTouchId();
|
|
625
|
+
const orderSpin = spinner('Placing order…');
|
|
626
|
+
const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na' });
|
|
627
|
+
orderSpin.stop();
|
|
628
|
+
assertApiOk(orderRes, 'Order placement failed');
|
|
629
|
+
success('Order submitted!');
|
|
630
|
+
printTxResult(orderRes.data);
|
|
631
|
+
}));
|
|
632
|
+
/** Try to extract a tradeable recommendation from the AI analysis response. */
|
|
633
|
+
function extractRecommendation(data, symbol, marginUSD, leverage) {
|
|
634
|
+
if (typeof data === 'string') {
|
|
635
|
+
return parseRecommendationText(data, symbol, marginUSD, leverage);
|
|
636
|
+
}
|
|
637
|
+
// Structured response — look for common field names
|
|
638
|
+
const flat = flattenObj(data);
|
|
639
|
+
const sideRaw = String(flat['recommendation'] ?? flat['direction'] ?? flat['side'] ?? flat['signal']
|
|
640
|
+
?? flat['action'] ?? flat['position'] ?? '').toLowerCase();
|
|
641
|
+
let side = null;
|
|
642
|
+
if (/long|buy|bullish/i.test(sideRaw))
|
|
643
|
+
side = 'buy';
|
|
644
|
+
else if (/short|sell|bearish/i.test(sideRaw))
|
|
645
|
+
side = 'sell';
|
|
646
|
+
if (!side) {
|
|
647
|
+
// Try to infer from the full JSON text
|
|
648
|
+
const jsonStr = JSON.stringify(data).toLowerCase();
|
|
649
|
+
if (/\blong\b|bullish/.test(jsonStr))
|
|
650
|
+
side = 'buy';
|
|
651
|
+
else if (/\bshort\b|bearish/.test(jsonStr))
|
|
652
|
+
side = 'sell';
|
|
653
|
+
}
|
|
654
|
+
if (!side)
|
|
655
|
+
return null;
|
|
656
|
+
const entryPrice = Number(flat['entryPrice'] ?? flat['entry_price'] ?? flat['entry'] ?? flat['price']
|
|
657
|
+
?? flat['currentPrice'] ?? flat['current_price'] ?? flat['markPrice'] ?? 0);
|
|
658
|
+
if (!entryPrice || entryPrice <= 0)
|
|
659
|
+
return null;
|
|
660
|
+
let size = Number(flat['size'] ?? flat['contracts'] ?? flat['qty'] ?? flat['quantity'] ?? 0);
|
|
661
|
+
if (!size || size <= 0) {
|
|
662
|
+
size = parseFloat(((marginUSD * leverage) / entryPrice).toPrecision(4));
|
|
663
|
+
}
|
|
664
|
+
if (!size || size <= 0)
|
|
665
|
+
return null;
|
|
666
|
+
return { side, entryPrice, size };
|
|
667
|
+
}
|
|
668
|
+
function parseRecommendationText(text, symbol, marginUSD, leverage) {
|
|
669
|
+
let side = null;
|
|
670
|
+
if (/\blong\b|bullish|buy/i.test(text))
|
|
671
|
+
side = 'buy';
|
|
672
|
+
else if (/\bshort\b|bearish|sell/i.test(text))
|
|
673
|
+
side = 'sell';
|
|
674
|
+
if (!side)
|
|
675
|
+
return null;
|
|
676
|
+
const priceMatch = text.match(/entry[:\s]*\$?([\d,.]+)/i)
|
|
677
|
+
?? text.match(/price[:\s]*\$?([\d,.]+)/i)
|
|
678
|
+
?? text.match(/\$\s*([\d,.]+)/);
|
|
679
|
+
const entryPrice = priceMatch ? Number(priceMatch[1].replace(/,/g, '')) : 0;
|
|
680
|
+
if (!entryPrice || entryPrice <= 0)
|
|
681
|
+
return null;
|
|
682
|
+
const size = parseFloat(((marginUSD * leverage) / entryPrice).toPrecision(4));
|
|
683
|
+
if (!size || size <= 0)
|
|
684
|
+
return null;
|
|
685
|
+
return { side, entryPrice, size };
|
|
686
|
+
}
|
|
687
|
+
/** Recursively flatten nested object keys for easier field lookup. */
|
|
688
|
+
function flattenObj(obj, prefix = '') {
|
|
689
|
+
const result = {};
|
|
690
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
691
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
692
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
693
|
+
Object.assign(result, flattenObj(v, key));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
result[k] = v;
|
|
697
|
+
result[key] = v;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
258
702
|
// ═════════════════════════════════════════════════════════════════════════
|
|
259
703
|
// Parent
|
|
260
704
|
// ═════════════════════════════════════════════════════════════════════════
|
|
261
705
|
export const perpsCommand = new Command('perps')
|
|
262
|
-
.description('Hyperliquid perpetual futures —
|
|
263
|
-
.addCommand(depositCmd)
|
|
264
|
-
.addCommand(withdrawCmd)
|
|
706
|
+
.description('Hyperliquid perpetual futures — order, positions, autopilot, analysis')
|
|
265
707
|
.addCommand(positionsCmd)
|
|
266
708
|
.addCommand(orderCmd)
|
|
267
709
|
.addCommand(cancelCmd)
|
|
268
710
|
.addCommand(leverageCmd)
|
|
269
711
|
.addCommand(tradesCmd)
|
|
712
|
+
.addCommand(depositCmd)
|
|
713
|
+
.addCommand(withdrawCmd)
|
|
270
714
|
.addCommand(fundRecordsCmd)
|
|
715
|
+
.addCommand(autopilotCmd)
|
|
716
|
+
.addCommand(askCmd)
|
|
271
717
|
.action(wrapAction(async () => {
|
|
718
|
+
const creds = requireAuth();
|
|
719
|
+
// Show autopilot status inline
|
|
720
|
+
const apState = await getAutopilotState(creds.accessToken);
|
|
721
|
+
const apLabel = apState.active ? chalk.green.bold(' [ON]') : chalk.dim(' [OFF]');
|
|
272
722
|
const action = await select({
|
|
273
723
|
message: 'Perps — what would you like to do?',
|
|
274
724
|
choices: [
|
|
275
|
-
{ name: '
|
|
276
|
-
{ name: 'Withdraw USDC', value: 'withdraw' },
|
|
725
|
+
{ name: 'View positions', value: 'positions' },
|
|
277
726
|
{ name: 'Place order', value: 'order' },
|
|
278
727
|
{ name: 'Cancel order', value: 'cancel' },
|
|
279
|
-
{ name: 'View positions', value: 'positions' },
|
|
280
728
|
{ name: 'Update leverage', value: 'leverage' },
|
|
281
|
-
{ name: 'View
|
|
729
|
+
{ name: 'View trade history', value: 'trades' },
|
|
730
|
+
{ name: 'Deposit USDC', value: 'deposit' },
|
|
731
|
+
{ name: 'Withdraw USDC', value: 'withdraw' },
|
|
282
732
|
{ name: 'Fund records', value: 'fund-records' },
|
|
733
|
+
{ name: `Autopilot${apLabel}`, value: 'autopilot' },
|
|
734
|
+
{ name: 'Ask AI (long/short analysis)', value: 'ask' },
|
|
283
735
|
],
|
|
284
736
|
});
|
|
285
737
|
const sub = perpsCommand.commands.find((c) => c.name() === action || c.aliases().includes(action));
|
package/dist/formatters.d.ts
CHANGED
|
@@ -40,6 +40,10 @@ export declare function printTxResult(data: unknown): void;
|
|
|
40
40
|
export declare const SPOT_COLUMNS: ColumnDef[];
|
|
41
41
|
/** Perps positions — API uses snake_case field names */
|
|
42
42
|
export declare const POSITION_COLUMNS: ColumnDef[];
|
|
43
|
+
/** Completed perps trades */
|
|
44
|
+
export declare const TRADE_COLUMNS: ColumnDef[];
|
|
45
|
+
/** Hyperliquid user fills (from public API) */
|
|
46
|
+
export declare const FILL_COLUMNS: ColumnDef[];
|
|
43
47
|
/** Limit orders (LimitOrderInfo[]) */
|
|
44
48
|
export declare const LIMIT_ORDER_COLUMNS: ColumnDef[];
|
|
45
49
|
/** Trending / search tokens (TokenInfo[]) */
|
package/dist/formatters.js
CHANGED
|
@@ -272,6 +272,72 @@ export const POSITION_COLUMNS = [
|
|
|
272
272
|
{ key: 'leverage', label: 'Lev', format: (v) => v ? `${v}x` : chalk.dim('—') },
|
|
273
273
|
{ key: 'marginUsed', label: 'Margin', format: (v) => formatValue(v, 'price') },
|
|
274
274
|
];
|
|
275
|
+
/** Completed perps trades */
|
|
276
|
+
export const TRADE_COLUMNS = [
|
|
277
|
+
{ key: 'symbol', label: 'Symbol', format: (v) => chalk.bold(String(v ?? '—').replace('USDT', '')) },
|
|
278
|
+
{ key: 'side', label: 'Side', format: (v) => {
|
|
279
|
+
const s = String(v ?? '').toLowerCase();
|
|
280
|
+
return s === 'long' || s === 'buy' ? chalk.green.bold(String(v)) : chalk.red.bold(String(v));
|
|
281
|
+
} },
|
|
282
|
+
{ key: 'quantity', label: 'Size', format: (v) => {
|
|
283
|
+
const n = Number(v);
|
|
284
|
+
return isNaN(n) ? String(v ?? '—') : n.toLocaleString('en-US', { maximumFractionDigits: 4 });
|
|
285
|
+
} },
|
|
286
|
+
{ key: 'open_price', label: 'Open', format: (v) => formatValue(v, 'price') },
|
|
287
|
+
{ key: 'close_price', label: 'Close', format: (v) => formatValue(v, 'price') },
|
|
288
|
+
{ key: 'pnl', label: 'PnL', format: (v) => {
|
|
289
|
+
if (!v && v !== 0)
|
|
290
|
+
return chalk.dim('—');
|
|
291
|
+
const n = Number(v);
|
|
292
|
+
const color = n >= 0 ? chalk.green : chalk.red;
|
|
293
|
+
return color(`${n >= 0 ? '+' : ''}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
|
294
|
+
} },
|
|
295
|
+
{ key: 'duration', label: 'Duration' },
|
|
296
|
+
{ key: 'close_time', label: 'Closed', format: (v) => {
|
|
297
|
+
if (!v)
|
|
298
|
+
return chalk.dim('—');
|
|
299
|
+
const d = new Date(String(v));
|
|
300
|
+
return isNaN(d.getTime()) ? String(v) : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
301
|
+
} },
|
|
302
|
+
];
|
|
303
|
+
/** Hyperliquid user fills (from public API) */
|
|
304
|
+
export const FILL_COLUMNS = [
|
|
305
|
+
{ key: 'coin', label: 'Asset', format: (v) => chalk.bold(String(v ?? '—')) },
|
|
306
|
+
{ key: 'dir', label: 'Direction', format: (v) => {
|
|
307
|
+
const s = String(v ?? '');
|
|
308
|
+
if (/open.*long|buy/i.test(s))
|
|
309
|
+
return chalk.green.bold(s);
|
|
310
|
+
if (/close.*short/i.test(s))
|
|
311
|
+
return chalk.green(s);
|
|
312
|
+
if (/open.*short|sell/i.test(s))
|
|
313
|
+
return chalk.red.bold(s);
|
|
314
|
+
if (/close.*long/i.test(s))
|
|
315
|
+
return chalk.red(s);
|
|
316
|
+
return s;
|
|
317
|
+
} },
|
|
318
|
+
{ key: 'sz', label: 'Size', format: (v) => {
|
|
319
|
+
const n = Number(v);
|
|
320
|
+
return isNaN(n) ? String(v ?? '—') : n.toLocaleString('en-US', { maximumFractionDigits: 4 });
|
|
321
|
+
} },
|
|
322
|
+
{ key: 'px', label: 'Price', format: (v) => formatValue(v, 'price') },
|
|
323
|
+
{ key: 'closedPnl', label: 'Realized PnL', format: (v) => {
|
|
324
|
+
const n = Number(v ?? 0);
|
|
325
|
+
if (n === 0)
|
|
326
|
+
return chalk.dim('—');
|
|
327
|
+
const color = n >= 0 ? chalk.green : chalk.red;
|
|
328
|
+
return color(`${n >= 0 ? '+' : ''}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
|
329
|
+
} },
|
|
330
|
+
{ key: 'fee', label: 'Fee', format: (v) => {
|
|
331
|
+
const n = Number(v ?? 0);
|
|
332
|
+
return n !== 0 ? `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 })}` : chalk.dim('—');
|
|
333
|
+
} },
|
|
334
|
+
{ key: 'time', label: 'Time', format: (v) => {
|
|
335
|
+
if (!v)
|
|
336
|
+
return chalk.dim('—');
|
|
337
|
+
const d = new Date(Number(v));
|
|
338
|
+
return isNaN(d.getTime()) ? String(v) : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
339
|
+
} },
|
|
340
|
+
];
|
|
275
341
|
/** Limit orders (LimitOrderInfo[]) */
|
|
276
342
|
export const LIMIT_ORDER_COLUMNS = [
|
|
277
343
|
{ key: 'id', label: 'ID', format: (v) => chalk.dim(truncate(String(v ?? ''), 12)) },
|