@invizi/cli 0.1.5 → 0.1.7

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.
@@ -0,0 +1 @@
1
+ export { trades } from './trades.js';
@@ -0,0 +1,289 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { loadConfig } from '../config.js';
3
+ const HLP_API = 'https://api.hyperliquid.xyz/info';
4
+ const MAX_FILLS_PER_REQUEST = 2000;
5
+ const REQUEST_DELAY_MS = 200;
6
+ async function fetchFills(address, startTime, endTime) {
7
+ const body = {
8
+ type: 'userFillsByTime',
9
+ user: address,
10
+ startTime,
11
+ aggregateByTime: true,
12
+ };
13
+ if (endTime)
14
+ body.endTime = endTime;
15
+ const res = await fetch(HLP_API, {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify(body),
19
+ });
20
+ if (!res.ok) {
21
+ throw new Error(`Hyperliquid API error: ${res.status}`);
22
+ }
23
+ return (await res.json());
24
+ }
25
+ async function fetchAllFills(address, startTime) {
26
+ const allFills = [];
27
+ let cursor = startTime;
28
+ while (true) {
29
+ const fills = await fetchFills(address, cursor);
30
+ if (fills.length === 0)
31
+ break;
32
+ allFills.push(...fills);
33
+ if (fills.length < MAX_FILLS_PER_REQUEST)
34
+ break;
35
+ // Next page: start after last fill's time
36
+ cursor = fills[fills.length - 1].time + 1;
37
+ // Rate limit courtesy
38
+ await new Promise(r => setTimeout(r, REQUEST_DELAY_MS));
39
+ }
40
+ return allFills;
41
+ }
42
+ function computeStats(fills) {
43
+ const byCoin = new Map();
44
+ for (const fill of fills) {
45
+ const existing = byCoin.get(fill.coin) || [];
46
+ existing.push(fill);
47
+ byCoin.set(fill.coin, existing);
48
+ }
49
+ const stats = [];
50
+ for (const [coin, coinFills] of byCoin) {
51
+ let grossPnl = 0;
52
+ let fees = 0;
53
+ let volume = 0;
54
+ let trades = 0;
55
+ let wins = 0;
56
+ let losses = 0;
57
+ // Track trade cycles: accumulate closedPnl until position flips through zero
58
+ let cyclePnl = 0;
59
+ let inTrade = false;
60
+ for (const fill of coinFills) {
61
+ const pnl = parseFloat(fill.closedPnl);
62
+ const fee = parseFloat(fill.fee);
63
+ const size = parseFloat(fill.sz);
64
+ const price = parseFloat(fill.px);
65
+ const startPos = parseFloat(fill.startPosition);
66
+ grossPnl += pnl;
67
+ fees += fee;
68
+ volume += size * price;
69
+ if (pnl !== 0) {
70
+ // This is a closing fill
71
+ cyclePnl += pnl - fee;
72
+ // Check if position went to zero (trade cycle complete)
73
+ const endPos = fill.side === 'B'
74
+ ? startPos + size
75
+ : startPos - size;
76
+ if (Math.abs(endPos) < 0.0000001 || Math.sign(endPos) !== Math.sign(startPos)) {
77
+ // Position closed or flipped
78
+ trades++;
79
+ if (cyclePnl > 0)
80
+ wins++;
81
+ else
82
+ losses++;
83
+ cyclePnl = 0;
84
+ inTrade = false;
85
+ }
86
+ else {
87
+ inTrade = true;
88
+ }
89
+ }
90
+ else if (!inTrade) {
91
+ inTrade = true;
92
+ }
93
+ }
94
+ // If there's an unclosed cycle with realized P&L, count it
95
+ if (inTrade && cyclePnl !== 0) {
96
+ trades++;
97
+ if (cyclePnl > 0)
98
+ wins++;
99
+ else
100
+ losses++;
101
+ }
102
+ // Fallback: if no trade cycles detected but there's P&L, count closing fills
103
+ if (trades === 0 && grossPnl !== 0) {
104
+ const closingFills = coinFills.filter(f => parseFloat(f.closedPnl) !== 0);
105
+ trades = closingFills.length;
106
+ wins = closingFills.filter(f => parseFloat(f.closedPnl) > 0).length;
107
+ losses = closingFills.filter(f => parseFloat(f.closedPnl) <= 0).length;
108
+ }
109
+ if (trades > 0 || grossPnl !== 0) {
110
+ stats.push({
111
+ coin,
112
+ trades,
113
+ wins,
114
+ losses,
115
+ grossPnl,
116
+ fees,
117
+ netPnl: grossPnl - fees,
118
+ volume,
119
+ lastTrade: coinFills[coinFills.length - 1].time,
120
+ });
121
+ }
122
+ }
123
+ // Sort by absolute net P&L descending
124
+ stats.sort((a, b) => Math.abs(b.netPnl) - Math.abs(a.netPnl));
125
+ return stats;
126
+ }
127
+ function formatUsd(n) {
128
+ if (n >= 0)
129
+ return `+$${n.toFixed(2)}`;
130
+ return `-$${Math.abs(n).toFixed(2)}`;
131
+ }
132
+ function formatTable(stats, address, days) {
133
+ const lines = [];
134
+ const addrShort = `${address.slice(0, 6)}...${address.slice(-4)}`;
135
+ lines.push(`Trade Analysis for ${addrShort} (Hyperliquid)`);
136
+ if (days)
137
+ lines.push(`Period: last ${days} days`);
138
+ lines.push('');
139
+ if (stats.length === 0) {
140
+ lines.push(' No closed trades found.');
141
+ return lines.join('\n');
142
+ }
143
+ // Header
144
+ const header = ' Coin Trades Win% Gross P&L Fees Net P&L';
145
+ lines.push(header);
146
+ lines.push(' ' + '─'.repeat(header.length - 2));
147
+ let totalTrades = 0;
148
+ let totalWins = 0;
149
+ let totalGross = 0;
150
+ let totalFees = 0;
151
+ let totalNet = 0;
152
+ for (const s of stats) {
153
+ const winPct = s.trades > 0 ? Math.round((s.wins / s.trades) * 100) : 0;
154
+ const feeStr = `-$${s.fees.toFixed(2)}`;
155
+ const line = ` ${s.coin.padEnd(12)} ${String(s.trades).padStart(6)} ${String(winPct + '%').padStart(4)} ${formatUsd(s.grossPnl).padStart(12)} ${feeStr.padStart(8)} ${formatUsd(s.netPnl).padStart(12)}`;
156
+ lines.push(line);
157
+ totalTrades += s.trades;
158
+ totalWins += s.wins;
159
+ totalGross += s.grossPnl;
160
+ totalFees += s.fees;
161
+ totalNet += s.netPnl;
162
+ }
163
+ lines.push(' ' + '─'.repeat(header.length - 2));
164
+ const totalWinPct = totalTrades > 0 ? Math.round((totalWins / totalTrades) * 100) : 0;
165
+ const totalFeeStr = `-$${totalFees.toFixed(2)}`;
166
+ lines.push(` ${'Total'.padEnd(12)} ${String(totalTrades).padStart(6)} ${String(totalWinPct + '%').padStart(4)} ${formatUsd(totalGross).padStart(12)} ${totalFeeStr.padStart(8)} ${formatUsd(totalNet).padStart(12)}`);
167
+ lines.push('');
168
+ // Top winner / loser
169
+ const best = stats.reduce((a, b) => a.netPnl > b.netPnl ? a : b);
170
+ const worst = stats.reduce((a, b) => a.netPnl < b.netPnl ? a : b);
171
+ if (best.netPnl > 0)
172
+ lines.push(` Best: ${best.coin} ${formatUsd(best.netPnl)}`);
173
+ if (worst.netPnl < 0)
174
+ lines.push(` Worst: ${worst.coin} ${formatUsd(worst.netPnl)}`);
175
+ return lines.join('\n');
176
+ }
177
+ function formatCoinDetail(fills, coin) {
178
+ const coinFills = fills
179
+ .filter(f => f.coin.toUpperCase() === coin.toUpperCase())
180
+ .filter(f => parseFloat(f.closedPnl) !== 0);
181
+ if (coinFills.length === 0) {
182
+ return `No closed trades found for ${coin}.`;
183
+ }
184
+ const lines = [];
185
+ lines.push(`${coin} Trade History`);
186
+ lines.push('');
187
+ lines.push(' Date Dir Size Price P&L Fee');
188
+ lines.push(' ' + '─'.repeat(78));
189
+ for (const f of coinFills) {
190
+ const date = new Date(f.time).toISOString().replace('T', ' ').slice(0, 19);
191
+ const pnl = parseFloat(f.closedPnl);
192
+ const fee = parseFloat(f.fee);
193
+ const line = ` ${date} ${f.dir.padEnd(14)} ${parseFloat(f.sz).toFixed(4).padStart(10)} ${parseFloat(f.px).toFixed(2).padStart(10)} ${formatUsd(pnl).padStart(9)} ${formatUsd(-fee).padStart(8)}`;
194
+ lines.push(line);
195
+ }
196
+ return lines.join('\n');
197
+ }
198
+ function showHelp() {
199
+ console.log(`
200
+ invizi trades — Analyze past trades from Hyperliquid
201
+
202
+ Usage:
203
+ invizi trades [address] [options]
204
+
205
+ Options:
206
+ --days <n> Only show trades from the last N days
207
+ --coin <COIN> Show per-fill detail for a specific coin
208
+ --json Output as JSON (for piping to AI tools)
209
+ --help Show this help
210
+
211
+ Examples:
212
+ invizi trades P&L by coin (connected address)
213
+ invizi trades --days 30 Last 30 days only
214
+ invizi trades --coin SOL SOL fill-by-fill detail
215
+ invizi trades 0xABC...123 Analyze any public address
216
+ invizi trades --days 7 --json JSON output for AI tools
217
+
218
+ Data is read from the public Hyperliquid blockchain. No auth required.
219
+ `);
220
+ }
221
+ export async function trades(args) {
222
+ const { values, positionals } = parseArgs({
223
+ args,
224
+ options: {
225
+ days: { type: 'string', short: 'd' },
226
+ coin: { type: 'string', short: 'c' },
227
+ json: { type: 'boolean', default: false },
228
+ help: { type: 'boolean', short: 'h', default: false },
229
+ },
230
+ allowPositionals: true,
231
+ strict: false,
232
+ });
233
+ if (values.help) {
234
+ showHelp();
235
+ return 0;
236
+ }
237
+ const config = loadConfig();
238
+ const address = positionals.find(a => /^0x[0-9a-fA-F]{40}$/.test(a)) || config.address;
239
+ if (!address) {
240
+ console.error('No address found. Run: invizi connect <address>');
241
+ console.error('Or pass directly: invizi trades 0x...');
242
+ return 1;
243
+ }
244
+ const days = typeof values.days === 'string' ? parseInt(values.days, 10) : null;
245
+ const coinFilter = typeof values.coin === 'string' ? values.coin.toUpperCase() : null;
246
+ const jsonMode = values.json === true;
247
+ // Calculate start time
248
+ const now = Date.now();
249
+ const startTime = days ? now - days * 24 * 60 * 60 * 1000 : 0;
250
+ const addrShort = `${address.slice(0, 6)}...${address.slice(-4)}`;
251
+ if (!jsonMode) {
252
+ console.log(`Fetching trades for ${addrShort}...`);
253
+ }
254
+ try {
255
+ const fills = await fetchAllFills(address, startTime);
256
+ if (fills.length === 0) {
257
+ console.log('No trades found.');
258
+ return 0;
259
+ }
260
+ if (!jsonMode) {
261
+ console.log(` ${fills.length} fills loaded.\n`);
262
+ }
263
+ if (coinFilter) {
264
+ if (jsonMode) {
265
+ const coinFills = fills.filter(f => f.coin.toUpperCase() === coinFilter);
266
+ const stats = computeStats(coinFills);
267
+ console.log(JSON.stringify(stats, null, 2));
268
+ }
269
+ else {
270
+ console.log(formatCoinDetail(fills, coinFilter));
271
+ }
272
+ }
273
+ else {
274
+ const stats = computeStats(fills);
275
+ if (jsonMode) {
276
+ console.log(JSON.stringify(stats, null, 2));
277
+ }
278
+ else {
279
+ console.log(formatTable(stats, address, days));
280
+ }
281
+ }
282
+ return 0;
283
+ }
284
+ catch (error) {
285
+ const message = error instanceof Error ? error.message : String(error);
286
+ console.error(`Error: ${message}`);
287
+ return 1;
288
+ }
289
+ }
package/dist/invizi.js CHANGED
@@ -3,20 +3,27 @@ import { auth, getAuthorizationHeader } from './auth.js';
3
3
  import { connect } from './connect.js';
4
4
  import { getApiUrl, getConfigPath, loadConfig, redactConfig } from './config.js';
5
5
  import { setup } from './setup.js';
6
+ import { trades } from './commands/index.js';
6
7
  function showLocalHelp() {
7
8
  console.log(`
8
9
  Invizi CLI
9
10
 
10
- Commands:
11
- invizi auth login Login via Auth0 device flow
12
- invizi auth status Show current auth identity
13
- invizi auth logout Clear local Auth0 session
14
- invizi connect <address> Connect a wallet address (auto-detects exchange)
15
- invizi setup Install skills into your AI tool
11
+ Data:
12
+ invizi context Market regime, macro, yields, options, HLP
13
+ invizi context -c SOL + coin data, flow, positions, trade history
14
+ invizi scout <COIN> Quick coin scan (price, funding, RSI, OBI)
15
+ invizi market BTC/ETH/SOL prices, fear/greed, funding extremes
16
+ invizi trades Analyze past trades P&L by coin (local)
17
+
18
+ Account:
19
+ invizi auth login Login
20
+ invizi auth status Show current identity
21
+ invizi connect <address> Connect a wallet address
22
+
23
+ Setup:
24
+ invizi setup Configure API + install AI skills
16
25
  invizi config Show current config
17
26
  invizi version Show CLI version
18
-
19
- Run invizi --help after login for the remote command list.
20
27
  `);
21
28
  }
22
29
  async function executeProtocol(args, authHeader) {
@@ -74,13 +81,12 @@ async function showRemoteHelp(authHeader) {
74
81
  console.log('No commands available for this user.');
75
82
  return;
76
83
  }
77
- console.log('Invizi Remote Commands\n');
78
84
  for (const cmd of commands) {
79
85
  const name = Array.isArray(cmd.tokens) ? cmd.tokens.join(' ') : cmd.id || '(unknown)';
80
86
  const desc = cmd.description || '';
81
- console.log(` ${name.padEnd(20)} ${desc}`);
87
+ console.log(` invizi ${name.padEnd(22)} ${desc}`);
82
88
  }
83
- console.log('\nUse: invizi <command> --help (if supported)');
89
+ console.log('');
84
90
  }
85
91
  async function executeRemote(args, options = {}) {
86
92
  const authHeader = await getAuthorizationHeader();
@@ -127,6 +133,8 @@ export async function main(rawArgs = process.argv.slice(2)) {
127
133
  return auth(args.slice(1));
128
134
  if (command === 'connect')
129
135
  return connect(args.slice(1));
136
+ if (command === 'trades')
137
+ return trades(args.slice(1));
130
138
  if (command === 'setup')
131
139
  return setup(args.slice(1));
132
140
  if (command === 'config') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invizi/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Invizi CLI",
5
5
  "type": "module",
6
6
  "bin": {