perp-cli 0.4.2 → 0.4.8
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 +2 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +7 -4
- package/dist/commands/arb-auto.js +643 -4
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/manage.js +1 -1
- package/dist/commands/market.js +1 -1
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.js +29 -8
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -0
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
- package/dist/exchanges/hyperliquid-spot.js +317 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +171 -73
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +128 -35
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +52 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +147 -98
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.js +14 -3
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +93 -4
- package/skills/perp-cli/references/commands.md +16 -1
- package/skills/perp-cli/references/strategies.md +1 -1
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PnL Tracker CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* perp pnl snapshot — take one-time equity snapshots across exchanges
|
|
5
|
+
* perp pnl track — continuous equity tracking (foreground)
|
|
6
|
+
* perp pnl daily — daily PnL breakdown table
|
|
7
|
+
* perp pnl weekly — weekly PnL breakdown table
|
|
8
|
+
* perp pnl summary — performance summary (Sharpe, drawdown, win rate)
|
|
9
|
+
*/
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { printJson, jsonOk, jsonError, makeTable, formatUsd, formatPnl, withJsonErrors } from "../utils.js";
|
|
12
|
+
import { saveEquitySnapshot, readEquityHistory, computePnlMetrics, computeDailyPnl, aggregateWeekly, } from "../equity-tracker.js";
|
|
13
|
+
const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
|
|
14
|
+
function parseSince(since) {
|
|
15
|
+
if (!since)
|
|
16
|
+
return undefined;
|
|
17
|
+
const match = since.match(/^(\d+)(h|d|w)$/);
|
|
18
|
+
if (match) {
|
|
19
|
+
const [, num, unit] = match;
|
|
20
|
+
const ms = { h: 3600000, d: 86400000, w: 604800000 }[unit] ?? 86400000;
|
|
21
|
+
return new Date(Date.now() - parseInt(num) * ms);
|
|
22
|
+
}
|
|
23
|
+
return new Date(since);
|
|
24
|
+
}
|
|
25
|
+
/** Take equity snapshot for given adapters. */
|
|
26
|
+
async function takeSnapshot(getAdapterForExchange, exchanges) {
|
|
27
|
+
const ts = new Date().toISOString();
|
|
28
|
+
const snaps = [];
|
|
29
|
+
const results = await Promise.allSettled(exchanges.map(async (ex) => {
|
|
30
|
+
const adapter = await getAdapterForExchange(ex);
|
|
31
|
+
const [balance, positions] = await Promise.all([
|
|
32
|
+
adapter.getBalance(),
|
|
33
|
+
adapter.getPositions(),
|
|
34
|
+
]);
|
|
35
|
+
const snap = {
|
|
36
|
+
ts,
|
|
37
|
+
exchange: ex,
|
|
38
|
+
equity: Number(balance.equity),
|
|
39
|
+
available: Number(balance.available),
|
|
40
|
+
marginUsed: Number(balance.marginUsed),
|
|
41
|
+
unrealizedPnl: Number(balance.unrealizedPnl),
|
|
42
|
+
positionCount: positions.filter(p => Number(p.size) > 0).length,
|
|
43
|
+
};
|
|
44
|
+
saveEquitySnapshot(snap);
|
|
45
|
+
return snap;
|
|
46
|
+
}));
|
|
47
|
+
for (const r of results) {
|
|
48
|
+
if (r.status === "fulfilled")
|
|
49
|
+
snaps.push(r.value);
|
|
50
|
+
}
|
|
51
|
+
return snaps;
|
|
52
|
+
}
|
|
53
|
+
export function registerPnlCommands(program, getAdapterForExchange, isJson) {
|
|
54
|
+
const pnl = program.command("pnl").description("PnL tracker — equity tracking & performance metrics");
|
|
55
|
+
// ── pnl snapshot ──
|
|
56
|
+
pnl
|
|
57
|
+
.command("snapshot")
|
|
58
|
+
.description("Take equity snapshot across all exchanges")
|
|
59
|
+
.option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
await withJsonErrors(isJson(), async () => {
|
|
62
|
+
const exchanges = opts.exchanges.split(",").map(e => e.trim());
|
|
63
|
+
const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
|
|
64
|
+
if (isJson()) {
|
|
65
|
+
printJson(jsonOk({ snapshots: snaps }));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const total = snaps.reduce((s, sn) => s + sn.equity, 0);
|
|
69
|
+
console.log(chalk.cyan.bold("Equity Snapshot\n"));
|
|
70
|
+
for (const s of snaps) {
|
|
71
|
+
console.log(` ${s.exchange.padEnd(14)} $${formatUsd(s.equity)} ` +
|
|
72
|
+
`avail $${formatUsd(s.available)} ` +
|
|
73
|
+
`uPnL ${formatPnl(s.unrealizedPnl)} ` +
|
|
74
|
+
`pos: ${s.positionCount}`);
|
|
75
|
+
}
|
|
76
|
+
console.log(`\n ${"Total".padEnd(14)} $${formatUsd(total)}`);
|
|
77
|
+
console.log(chalk.gray(`\n Saved at ${snaps[0]?.ts ?? "—"}`));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// ── pnl track ──
|
|
82
|
+
pnl
|
|
83
|
+
.command("track")
|
|
84
|
+
.description("Continuously track equity (foreground)")
|
|
85
|
+
.option("--interval <sec>", "Snapshot interval in seconds", "300")
|
|
86
|
+
.option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
|
|
87
|
+
.action(async (opts) => {
|
|
88
|
+
const intervalMs = parseInt(opts.interval) * 1000;
|
|
89
|
+
const exchanges = opts.exchanges.split(",").map(e => e.trim());
|
|
90
|
+
console.log(chalk.cyan(`PnL tracker started — snapshot every ${opts.interval}s for [${exchanges.join(", ")}]`));
|
|
91
|
+
console.log(chalk.gray("Press Ctrl+C to stop\n"));
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
process.on("SIGINT", () => controller.abort());
|
|
94
|
+
// Initial snapshot
|
|
95
|
+
const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
|
|
96
|
+
const total = snaps.reduce((s, sn) => s + sn.equity, 0);
|
|
97
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} Total equity: $${formatUsd(total)}`);
|
|
98
|
+
while (!controller.signal.aborted) {
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
101
|
+
controller.signal.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
102
|
+
});
|
|
103
|
+
if (controller.signal.aborted)
|
|
104
|
+
break;
|
|
105
|
+
try {
|
|
106
|
+
const s = await takeSnapshot(getAdapterForExchange, exchanges);
|
|
107
|
+
const t = s.reduce((sum, sn) => sum + sn.equity, 0);
|
|
108
|
+
const delta = t - total;
|
|
109
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} ` +
|
|
110
|
+
`Total: $${formatUsd(t)} ${formatPnl(delta)}`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error(chalk.red(`Snapshot error: ${err instanceof Error ? err.message : err}`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// ── pnl daily ──
|
|
118
|
+
pnl
|
|
119
|
+
.command("daily")
|
|
120
|
+
.description("Daily PnL breakdown")
|
|
121
|
+
.option("--exchange <name>", "Single exchange filter")
|
|
122
|
+
.option("--days <n>", "Number of days to show", "14")
|
|
123
|
+
.option("--since <period>", "Time range (e.g. 7d, 2w, 30d)")
|
|
124
|
+
.action(async (opts) => {
|
|
125
|
+
await withJsonErrors(isJson(), async () => {
|
|
126
|
+
const since = parseSince(opts.since) ?? new Date(Date.now() - parseInt(opts.days) * 86400000);
|
|
127
|
+
const snapshots = readEquityHistory({ exchange: opts.exchange, since });
|
|
128
|
+
if (snapshots.length === 0) {
|
|
129
|
+
if (isJson()) {
|
|
130
|
+
printJson(jsonError("NO_DATA", "No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(chalk.yellow("No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const daily = computeDailyPnl(snapshots, opts.exchange);
|
|
138
|
+
if (isJson()) {
|
|
139
|
+
printJson(jsonOk({ daily }));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(chalk.cyan.bold(`Daily PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
|
|
143
|
+
const head = ["Date", "Start", "End", "PnL", "PnL %"];
|
|
144
|
+
const rows = daily.map(d => [
|
|
145
|
+
d.date,
|
|
146
|
+
`$${formatUsd(d.startEquity)}`,
|
|
147
|
+
`$${formatUsd(d.endEquity)}`,
|
|
148
|
+
formatPnl(d.pnl),
|
|
149
|
+
`${d.pnlPct >= 0 ? "+" : ""}${d.pnlPct.toFixed(2)}%`,
|
|
150
|
+
]);
|
|
151
|
+
console.log(makeTable(head, rows));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
// ── pnl weekly ──
|
|
156
|
+
pnl
|
|
157
|
+
.command("weekly")
|
|
158
|
+
.description("Weekly PnL breakdown")
|
|
159
|
+
.option("--exchange <name>", "Single exchange filter")
|
|
160
|
+
.option("--weeks <n>", "Number of weeks to show", "8")
|
|
161
|
+
.option("--since <period>", "Time range (e.g. 4w, 8w)")
|
|
162
|
+
.action(async (opts) => {
|
|
163
|
+
await withJsonErrors(isJson(), async () => {
|
|
164
|
+
const since = parseSince(opts.since) ?? new Date(Date.now() - parseInt(opts.weeks) * 7 * 86400000);
|
|
165
|
+
const snapshots = readEquityHistory({ exchange: opts.exchange, since });
|
|
166
|
+
if (snapshots.length === 0) {
|
|
167
|
+
if (isJson()) {
|
|
168
|
+
printJson(jsonError("NO_DATA", "No equity snapshots found."));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log(chalk.yellow("No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const daily = computeDailyPnl(snapshots, opts.exchange);
|
|
176
|
+
const weekly = aggregateWeekly(daily);
|
|
177
|
+
if (isJson()) {
|
|
178
|
+
printJson(jsonOk({ weekly }));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(chalk.cyan.bold(`Weekly PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
|
|
182
|
+
const head = ["Week", "Start", "End", "PnL", "PnL %"];
|
|
183
|
+
const rows = weekly.map(w => [
|
|
184
|
+
w.date,
|
|
185
|
+
`$${formatUsd(w.startEquity)}`,
|
|
186
|
+
`$${formatUsd(w.endEquity)}`,
|
|
187
|
+
formatPnl(w.pnl),
|
|
188
|
+
`${w.pnlPct >= 0 ? "+" : ""}${w.pnlPct.toFixed(2)}%`,
|
|
189
|
+
]);
|
|
190
|
+
console.log(makeTable(head, rows));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
// ── pnl summary ──
|
|
195
|
+
pnl
|
|
196
|
+
.command("summary")
|
|
197
|
+
.description("Performance summary — Sharpe, drawdown, win rate")
|
|
198
|
+
.option("--exchange <name>", "Single exchange filter")
|
|
199
|
+
.option("--since <period>", "Time range (e.g. 7d, 30d, 12w)")
|
|
200
|
+
.action(async (opts) => {
|
|
201
|
+
await withJsonErrors(isJson(), async () => {
|
|
202
|
+
const since = parseSince(opts.since);
|
|
203
|
+
const snapshots = readEquityHistory({ exchange: opts.exchange, since });
|
|
204
|
+
if (snapshots.length === 0) {
|
|
205
|
+
if (isJson()) {
|
|
206
|
+
printJson(jsonError("NO_DATA", "No equity snapshots found."));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log(chalk.yellow("No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const metrics = computePnlMetrics(snapshots, opts.exchange);
|
|
214
|
+
if (isJson()) {
|
|
215
|
+
printJson(jsonOk({
|
|
216
|
+
...metrics,
|
|
217
|
+
dailyReturns: undefined, // omit large array from JSON
|
|
218
|
+
dailyCount: metrics.dailyReturns.length,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.log(chalk.cyan.bold(`Performance Summary${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
|
|
223
|
+
console.log(` Period: ${metrics.period.from} → ${metrics.period.to} (${metrics.period.days} days)`);
|
|
224
|
+
console.log(` Total Return: ${formatPnl(metrics.totalReturn)} (${metrics.totalReturnPct >= 0 ? "+" : ""}${metrics.totalReturnPct.toFixed(2)}%)`);
|
|
225
|
+
console.log(` Peak Equity: $${formatUsd(metrics.peakEquity)}`);
|
|
226
|
+
console.log(` Avg Daily PnL: ${formatPnl(metrics.avgDailyPnl)}`);
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(` Sharpe Ratio: ${metrics.sharpeRatio >= 0 ? chalk.green(metrics.sharpeRatio.toFixed(2)) : chalk.red(metrics.sharpeRatio.toFixed(2))}`);
|
|
229
|
+
console.log(` Max Drawdown: ${chalk.red(`$${formatUsd(metrics.maxDrawdown)}`)} (${chalk.red(`-${metrics.maxDrawdownPct.toFixed(2)}%`)})`);
|
|
230
|
+
console.log(` Current DD: $${formatUsd(metrics.currentDrawdown)} (-${metrics.currentDrawdownPct.toFixed(2)}%)`);
|
|
231
|
+
console.log();
|
|
232
|
+
console.log(` Win Days: ${chalk.green(String(metrics.winDays))} / ${metrics.winDays + metrics.lossDays}`);
|
|
233
|
+
console.log(` Win Rate: ${metrics.winRate.toFixed(1)}%`);
|
|
234
|
+
if (metrics.bestDay) {
|
|
235
|
+
console.log(` Best Day: ${metrics.bestDay.date} ${formatPnl(metrics.bestDay.pnl)}`);
|
|
236
|
+
}
|
|
237
|
+
if (metrics.worstDay) {
|
|
238
|
+
console.log(` Worst Day: ${metrics.worstDay.date} ${formatPnl(metrics.worstDay.pnl)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
package/dist/commands/stream.js
CHANGED
|
@@ -1,93 +1,170 @@
|
|
|
1
1
|
import { PacificaWSClient } from "../pacifica/index.js";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { formatUsd, formatPercent } from "../utils.js";
|
|
4
|
+
/** Create an HL or Lighter market feed, connected and ready. */
|
|
5
|
+
async function getMarketFeed(exchange) {
|
|
6
|
+
const { createMarketFeed } = await import("../ws/market-feed.js");
|
|
7
|
+
const feed = createMarketFeed(exchange);
|
|
8
|
+
await feed.connect();
|
|
9
|
+
process.on("SIGINT", () => { feed.close(); process.exit(0); });
|
|
10
|
+
return feed;
|
|
11
|
+
}
|
|
4
12
|
export function registerStreamCommands(program, getNetwork, getExchange, getAdapter) {
|
|
5
13
|
const stream = program.command("stream").description("Live WebSocket streams (Ctrl+C to stop)");
|
|
14
|
+
// ── stream prices ──
|
|
6
15
|
stream
|
|
7
16
|
.command("prices")
|
|
8
17
|
.description("Stream live prices")
|
|
9
18
|
.action(async () => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
d.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
const exchange = getExchange();
|
|
20
|
+
if (exchange === "pacifica") {
|
|
21
|
+
const ws = new PacificaWSClient({ network: getNetwork() });
|
|
22
|
+
await ws.connect();
|
|
23
|
+
ws.subscribe("prices");
|
|
24
|
+
ws.on("prices", (msg) => {
|
|
25
|
+
const data = msg;
|
|
26
|
+
const d = data.data ?? data;
|
|
27
|
+
if (Array.isArray(d)) {
|
|
28
|
+
d.forEach((p) => {
|
|
29
|
+
const mark = Number(p.mark ?? p.m ?? 0);
|
|
30
|
+
const funding = Number(p.funding ?? p.f ?? 0);
|
|
31
|
+
const sym = String(p.symbol ?? p.s ?? "").padEnd(10);
|
|
32
|
+
console.log(`${chalk.white.bold(sym)} $${formatUsd(mark)} ${formatPercent(funding)}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
console.log(chalk.cyan("Streaming pacifica prices... (Ctrl+C to stop)\n"));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const feed = await getMarketFeed(exchange);
|
|
40
|
+
feed.subscribePrices();
|
|
41
|
+
feed.on("prices", (updates) => {
|
|
42
|
+
for (const p of updates) {
|
|
43
|
+
const sym = p.symbol.padEnd(10);
|
|
44
|
+
console.log(`${chalk.white.bold(sym)} $${formatUsd(p.mid)}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
console.log(chalk.cyan(`Streaming ${exchange} prices... (Ctrl+C to stop)\n`));
|
|
48
|
+
}
|
|
26
49
|
await new Promise(() => { }); // keep alive
|
|
27
50
|
});
|
|
51
|
+
// ── stream book ──
|
|
28
52
|
stream
|
|
29
53
|
.command("book <symbol>")
|
|
30
54
|
.description("Stream orderbook updates")
|
|
31
55
|
.action(async (symbol) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
const exchange = getExchange();
|
|
57
|
+
const sym = symbol.toUpperCase();
|
|
58
|
+
if (exchange === "pacifica") {
|
|
59
|
+
const ws = new PacificaWSClient({ network: getNetwork() });
|
|
60
|
+
await ws.connect();
|
|
61
|
+
ws.subscribe("book", { symbol: sym });
|
|
62
|
+
ws.on("book", (msg) => {
|
|
63
|
+
const data = msg;
|
|
64
|
+
console.log(`${chalk.cyan(new Date().toLocaleTimeString())} ${JSON.stringify(data.data ?? data)}`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const feed = await getMarketFeed(exchange);
|
|
69
|
+
feed.subscribeBook(sym);
|
|
70
|
+
feed.on("book", (data) => {
|
|
71
|
+
const topBids = data.bids.slice(0, 5).map(([p, s]) => `${p}x${s}`).join(" ");
|
|
72
|
+
const topAsks = data.asks.slice(0, 5).map(([p, s]) => `${p}x${s}`).join(" ");
|
|
73
|
+
console.log(`${chalk.cyan(new Date().toLocaleTimeString())} ${chalk.green("BID")} ${topBids} ${chalk.red("ASK")} ${topAsks}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.cyan(`Streaming ${sym} orderbook on ${exchange}... (Ctrl+C to stop)\n`));
|
|
40
77
|
await new Promise(() => { });
|
|
41
78
|
});
|
|
79
|
+
// ── stream trades ──
|
|
42
80
|
stream
|
|
43
81
|
.command("trades <symbol>")
|
|
44
82
|
.description("Stream live trades")
|
|
45
83
|
.action(async (symbol) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
84
|
+
const exchange = getExchange();
|
|
85
|
+
const sym = symbol.toUpperCase();
|
|
86
|
+
if (exchange === "pacifica") {
|
|
87
|
+
const ws = new PacificaWSClient({ network: getNetwork() });
|
|
88
|
+
await ws.connect();
|
|
89
|
+
ws.subscribe("trades", { symbol: sym });
|
|
90
|
+
ws.on("trades", (msg) => {
|
|
91
|
+
const data = msg;
|
|
92
|
+
const d = (data.data ?? data);
|
|
93
|
+
const side = String(d.side ?? d.d ?? "");
|
|
94
|
+
const price = String(d.price ?? d.p ?? "");
|
|
95
|
+
const amount = String(d.amount ?? d.a ?? "");
|
|
96
|
+
const color = side === "bid" || side === "buy" ? chalk.green : chalk.red;
|
|
97
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${color(side === "bid" ? "BUY " : "SELL")} $${formatUsd(price)} x ${amount}`);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const feed = await getMarketFeed(exchange);
|
|
102
|
+
feed.subscribeTrades(sym);
|
|
103
|
+
feed.on("trade", (t) => {
|
|
104
|
+
const color = t.side === "buy" ? chalk.green : chalk.red;
|
|
105
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${color(t.side === "buy" ? "BUY " : "SELL")} $${formatUsd(t.price)} x ${t.size}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
console.log(chalk.cyan(`Streaming ${sym} trades on ${exchange}... (Ctrl+C to stop)\n`));
|
|
59
109
|
await new Promise(() => { });
|
|
60
110
|
});
|
|
111
|
+
// ── stream bbo ──
|
|
61
112
|
stream
|
|
62
113
|
.command("bbo <symbol>")
|
|
63
114
|
.description("Stream best bid/offer")
|
|
64
115
|
.action(async (symbol) => {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
116
|
+
const exchange = getExchange();
|
|
117
|
+
const sym = symbol.toUpperCase();
|
|
118
|
+
if (exchange === "pacifica") {
|
|
119
|
+
const ws = new PacificaWSClient({ network: getNetwork() });
|
|
120
|
+
await ws.connect();
|
|
121
|
+
ws.subscribe("bbo", { symbol: sym });
|
|
122
|
+
ws.on("bbo", (msg) => {
|
|
123
|
+
const data = msg.data ?? msg;
|
|
124
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${JSON.stringify(data)}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// BBO derived from orderbook (HL/Lighter don't have dedicated BBO channel)
|
|
129
|
+
const feed = await getMarketFeed(exchange);
|
|
130
|
+
feed.subscribeBook(sym);
|
|
131
|
+
feed.on("book", (data) => {
|
|
132
|
+
const bid = data.bids[0] ?? ["—", "—"];
|
|
133
|
+
const ask = data.asks[0] ?? ["—", "—"];
|
|
134
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${chalk.green("bid")} ${bid[0]} (${bid[1]}) ${chalk.red("ask")} ${ask[0]} (${ask[1]})`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.cyan(`Streaming ${sym} BBO on ${exchange}... (Ctrl+C to stop)\n`));
|
|
73
138
|
await new Promise(() => { });
|
|
74
139
|
});
|
|
140
|
+
// ── stream candle ──
|
|
75
141
|
stream
|
|
76
142
|
.command("candle <symbol> <interval>")
|
|
77
143
|
.description("Stream candlestick data")
|
|
78
144
|
.action(async (symbol, interval) => {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
const exchange = getExchange();
|
|
146
|
+
const sym = symbol.toUpperCase();
|
|
147
|
+
if (exchange === "pacifica") {
|
|
148
|
+
const ws = new PacificaWSClient({ network: getNetwork() });
|
|
149
|
+
await ws.connect();
|
|
150
|
+
ws.subscribe("candle", { symbol: sym, interval });
|
|
151
|
+
ws.on("candle", (msg) => {
|
|
152
|
+
const data = msg.data ?? msg;
|
|
153
|
+
const d = data;
|
|
154
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} O:$${formatUsd(String(d.o ?? ""))} H:$${formatUsd(String(d.h ?? ""))} L:$${formatUsd(String(d.l ?? ""))} C:$${formatUsd(String(d.c ?? ""))} V:${d.v ?? ""}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const feed = await getMarketFeed(exchange);
|
|
159
|
+
feed.subscribeCandle(sym, interval);
|
|
160
|
+
feed.on("candle", (d) => {
|
|
161
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} O:$${formatUsd(d.o)} H:$${formatUsd(d.h)} L:$${formatUsd(d.l)} C:$${formatUsd(d.c)} V:${d.v}`);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
console.log(chalk.cyan(`Streaming ${sym} ${interval} candles on ${exchange}... (Ctrl+C to stop)\n`));
|
|
88
165
|
await new Promise(() => { });
|
|
89
166
|
});
|
|
90
|
-
// ── stream events ── (unified account events as NDJSON)
|
|
167
|
+
// ── stream events ── (unified account events as NDJSON, already exchange-agnostic)
|
|
91
168
|
if (getAdapter) {
|
|
92
169
|
stream
|
|
93
170
|
.command("events")
|
package/dist/commands/trade.js
CHANGED
|
@@ -245,21 +245,42 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
245
245
|
printJson(jsonOk(result));
|
|
246
246
|
});
|
|
247
247
|
trade
|
|
248
|
-
.command("cancel <
|
|
249
|
-
.description("Cancel
|
|
250
|
-
.action(async (
|
|
248
|
+
.command("cancel <symbolOrOrderId> [orderId]")
|
|
249
|
+
.description("Cancel an order by orderId (auto-detects symbol), or by symbol + orderId")
|
|
250
|
+
.action(async (symbolOrOrderId, orderId) => {
|
|
251
251
|
const adapter = await getAdapter();
|
|
252
|
-
|
|
252
|
+
let symbol;
|
|
253
|
+
let oid;
|
|
254
|
+
if (orderId) {
|
|
255
|
+
// cancel <symbol> <orderId>
|
|
256
|
+
symbol = symbolOrOrderId.toUpperCase();
|
|
257
|
+
oid = orderId;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// cancel <orderId> — look up symbol from open orders
|
|
261
|
+
oid = symbolOrOrderId;
|
|
262
|
+
const orders = await adapter.getOpenOrders();
|
|
263
|
+
const match = orders.find((o) => String(o.orderId) === oid);
|
|
264
|
+
if (!match) {
|
|
265
|
+
const err = `Order ${oid} not found in open orders`;
|
|
266
|
+
if (isJson())
|
|
267
|
+
return printJson(jsonOk({ cancelled: false, reason: err }));
|
|
268
|
+
console.log(chalk.yellow(`\n ${err}\n`));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
symbol = match.symbol;
|
|
272
|
+
}
|
|
273
|
+
if (dryRunGuard("cancel", { exchange: adapter.name, symbol, orderId: oid }))
|
|
253
274
|
return;
|
|
254
275
|
try {
|
|
255
|
-
const result = await adapter.cancelOrder(symbol
|
|
256
|
-
logExecution({ type: "cancel", exchange: adapter.name, symbol
|
|
276
|
+
const result = await adapter.cancelOrder(symbol, oid);
|
|
277
|
+
logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId: oid } });
|
|
257
278
|
if (isJson())
|
|
258
279
|
return printJson(jsonOk(result));
|
|
259
|
-
console.log(chalk.green(`\n Order ${
|
|
280
|
+
console.log(chalk.green(`\n Order ${oid} cancelled on ${adapter.name}.\n`));
|
|
260
281
|
}
|
|
261
282
|
catch (err) {
|
|
262
|
-
logExecution({ type: "cancel", exchange: adapter.name, symbol
|
|
283
|
+
logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId: oid } });
|
|
263
284
|
throw err;
|
|
264
285
|
}
|
|
265
286
|
});
|
package/dist/commands/wallet.js
CHANGED
|
@@ -389,7 +389,7 @@ export function registerWalletCommands(program, isJson) {
|
|
|
389
389
|
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
390
390
|
const adapter = new LighterAdapter(normalized);
|
|
391
391
|
await adapter.init();
|
|
392
|
-
const apiKeyIndex =
|
|
392
|
+
const apiKeyIndex = 4;
|
|
393
393
|
const { privateKey: apiKey } = await adapter.setupApiKey(apiKeyIndex);
|
|
394
394
|
setEnvVar("LIGHTER_API_KEY", apiKey);
|
|
395
395
|
setEnvVar("LIGHTER_ACCOUNT_INDEX", String(adapter.accountIndex));
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Keypair } from "@solana/web3.js";
|
|
2
2
|
export type Exchange = "pacifica" | "hyperliquid" | "lighter";
|
|
3
|
+
/**
|
|
4
|
+
* Try to load a private key without throwing if none is found.
|
|
5
|
+
* Returns null if no key is configured.
|
|
6
|
+
*/
|
|
7
|
+
export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string): Promise<string | null>;
|
|
3
8
|
export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string): Promise<string>;
|
|
4
9
|
export declare function parseSolanaKeypair(input: string): Keypair;
|
|
5
10
|
export declare function isEvmPrivateKey(input: string): boolean;
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,18 @@ import bs58 from "bs58";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
/**
|
|
7
|
+
* Try to load a private key without throwing if none is found.
|
|
8
|
+
* Returns null if no key is configured.
|
|
9
|
+
*/
|
|
10
|
+
export async function tryLoadPrivateKey(exchange, pkOverride) {
|
|
11
|
+
try {
|
|
12
|
+
return await loadPrivateKey(exchange, pkOverride);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
6
18
|
export async function loadPrivateKey(exchange, pkOverride) {
|
|
7
19
|
// 1. CLI flag
|
|
8
20
|
if (pkOverride)
|
|
@@ -569,7 +569,7 @@ class LighterFeed extends ExchangeWsFeed {
|
|
|
569
569
|
liquidationPrice: String(p.liquidation_price || "N/A"),
|
|
570
570
|
unrealizedPnl: String(p.unrealized_pnl ?? "0"),
|
|
571
571
|
leverage: Number(p.initial_margin_fraction ?? 0) > 0
|
|
572
|
-
? Math.round(
|
|
572
|
+
? Math.round(10000 / Number(p.initial_margin_fraction))
|
|
573
573
|
: 1,
|
|
574
574
|
};
|
|
575
575
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Equity Tracker — time-series equity snapshots for PnL analysis.
|
|
3
|
+
*
|
|
4
|
+
* Stores snapshots in ~/.perp/equity-history/YYYY-MM.jsonl (monthly files).
|
|
5
|
+
* Computes daily PnL, Sharpe ratio, max drawdown, win/loss stats.
|
|
6
|
+
*/
|
|
7
|
+
export interface EquitySnapshot {
|
|
8
|
+
ts: string;
|
|
9
|
+
exchange: string;
|
|
10
|
+
equity: number;
|
|
11
|
+
available: number;
|
|
12
|
+
marginUsed: number;
|
|
13
|
+
unrealizedPnl: number;
|
|
14
|
+
positionCount: number;
|
|
15
|
+
}
|
|
16
|
+
export interface DailyPnl {
|
|
17
|
+
date: string;
|
|
18
|
+
exchange: string;
|
|
19
|
+
startEquity: number;
|
|
20
|
+
endEquity: number;
|
|
21
|
+
pnl: number;
|
|
22
|
+
pnlPct: number;
|
|
23
|
+
}
|
|
24
|
+
export interface PnlMetrics {
|
|
25
|
+
totalReturn: number;
|
|
26
|
+
totalReturnPct: number;
|
|
27
|
+
dailyReturns: DailyPnl[];
|
|
28
|
+
sharpeRatio: number;
|
|
29
|
+
maxDrawdown: number;
|
|
30
|
+
maxDrawdownPct: number;
|
|
31
|
+
winDays: number;
|
|
32
|
+
lossDays: number;
|
|
33
|
+
winRate: number;
|
|
34
|
+
peakEquity: number;
|
|
35
|
+
currentDrawdown: number;
|
|
36
|
+
currentDrawdownPct: number;
|
|
37
|
+
bestDay: DailyPnl | null;
|
|
38
|
+
worstDay: DailyPnl | null;
|
|
39
|
+
avgDailyPnl: number;
|
|
40
|
+
period: {
|
|
41
|
+
from: string;
|
|
42
|
+
to: string;
|
|
43
|
+
days: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare function saveEquitySnapshot(snap: EquitySnapshot): void;
|
|
47
|
+
export declare function readEquityHistory(opts?: {
|
|
48
|
+
exchange?: string;
|
|
49
|
+
since?: Date;
|
|
50
|
+
until?: Date;
|
|
51
|
+
}): EquitySnapshot[];
|
|
52
|
+
/** Group snapshots by date, take first and last per day for daily PnL. */
|
|
53
|
+
export declare function computeDailyPnl(snapshots: EquitySnapshot[], exchange?: string): DailyPnl[];
|
|
54
|
+
/** Compute comprehensive PnL metrics from equity snapshots. */
|
|
55
|
+
export declare function computePnlMetrics(snapshots: EquitySnapshot[], exchange?: string): PnlMetrics;
|
|
56
|
+
/** Compute weekly PnL from daily PnL data. */
|
|
57
|
+
export declare function aggregateWeekly(daily: DailyPnl[]): DailyPnl[];
|