minara 0.2.4 → 0.2.5
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 +32 -21
- package/dist/api/perps.d.ts +58 -0
- package/dist/api/perps.js +97 -0
- 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
|
@@ -21,7 +21,8 @@
|
|
|
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
22
|
- **Wallet & Balance** — Unified balance view, spot holdings with PnL, perps account overview, deposits and withdrawals
|
|
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,14 +81,14 @@ 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
|
|
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 |
|
|
89
90
|
| `minara deposit` | Deposit to spot (view addresses) or perps (direct / from spot) |
|
|
90
|
-
| `minara withdraw` | Withdraw tokens to an external wallet
|
|
91
|
+
| `minara withdraw` | Withdraw tokens to an external wallet |
|
|
91
92
|
|
|
92
93
|
```bash
|
|
93
94
|
minara balance # Quick total: Spot + Perps available balance
|
|
@@ -121,25 +122,35 @@ minara swap --dry-run # Simulate without executing
|
|
|
121
122
|
|
|
122
123
|
### Perpetual Futures
|
|
123
124
|
|
|
124
|
-
| Command | Description
|
|
125
|
-
| --------------------------- |
|
|
125
|
+
| Command | Description |
|
|
126
|
+
| --------------------------- | ----------------------------------------------------- |
|
|
127
|
+
| `minara perps positions` | View all open positions with PnL |
|
|
128
|
+
| `minara perps order` | Place an order (interactive builder) |
|
|
129
|
+
| `minara perps cancel` | Cancel open orders |
|
|
130
|
+
| `minara perps leverage` | Update leverage for a symbol |
|
|
131
|
+
| `minara perps trades` | View trade history (Hyperliquid fills) |
|
|
126
132
|
| `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 |
|
|
133
|
+
| `minara perps withdraw` | Withdraw USDC from perps account |
|
|
134
|
+
| `minara perps fund-records` | View fund deposit/withdrawal records |
|
|
135
|
+
| `minara perps autopilot` | Manage AI autopilot trading strategy (on/off/config) |
|
|
136
|
+
| `minara perps ask` | AI long/short analysis with quick order |
|
|
134
137
|
|
|
135
138
|
```bash
|
|
139
|
+
minara perps positions # List positions with equity, margin, PnL
|
|
140
|
+
minara perps order # Interactive: symbol selector → side → size → confirm
|
|
141
|
+
minara perps leverage # Interactive: shows max leverage per asset
|
|
142
|
+
minara perps trades # Recent fills from Hyperliquid (default 7 days)
|
|
143
|
+
minara perps trades -d 30 # Last 30 days of trade history
|
|
136
144
|
minara perps deposit -a 100 # Deposit 100 USDC to perps
|
|
137
145
|
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
|
|
146
|
+
minara perps autopilot # Toggle AI autopilot, create/update strategy
|
|
147
|
+
minara perps ask # AI analysis → optional quick order
|
|
141
148
|
```
|
|
142
149
|
|
|
150
|
+
> **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`.
|
|
151
|
+
>
|
|
152
|
+
> **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.
|
|
153
|
+
|
|
143
154
|
### Limit Orders
|
|
144
155
|
|
|
145
156
|
| Command | Description |
|
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/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)) },
|