perp-cli 0.4.8 → 0.4.9
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 +44 -5
- package/dist/arb/reconcile.d.ts +28 -0
- package/dist/arb/reconcile.js +162 -0
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +300 -63
- package/dist/commands/agent.js +9 -5
- package/dist/commands/arb/index.js +243 -49
- package/dist/commands/arb-auto.js +67 -19
- package/dist/commands/arb.d.ts +2 -1
- package/dist/commands/arb.js +535 -131
- package/dist/commands/bot.js +207 -0
- package/dist/commands/funds.d.ts +4 -0
- package/dist/commands/funds.js +741 -0
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.js +508 -5
- package/dist/commands/manage.js +5 -3
- package/dist/commands/market.js +34 -1
- package/dist/commands/pnl.js +1 -1
- package/dist/commands/risk.d.ts +2 -0
- package/dist/commands/risk.js +56 -0
- package/dist/commands/settings.js +147 -0
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +241 -259
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +10 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +11 -3
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
- package/dist/exchanges/hyperliquid-spot.js +13 -21
- package/dist/exchanges/lighter-spot.js +13 -15
- package/dist/exchanges/lighter.js +82 -21
- package/dist/exchanges/pacifica.js +12 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/index.js +39 -123
- package/dist/mcp-server.js +43 -96
- package/dist/strategies/split-order.d.ts +49 -0
- package/dist/strategies/split-order.js +138 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +13 -26
- package/skills/perp-cli/references/agent-operations.md +19 -9
- package/skills/perp-cli/references/commands.md +38 -31
- package/skills/perp-cli/references/strategies.md +26 -16
- package/skills/perp-cli/scripts/funding-analysis.sh +2 -2
- package/skills/perp-cli/scripts/preflight.sh +1 -1
- package/skills/perp-cli/scripts/spot-perp-scan.sh +1 -1
package/dist/commands/trade.js
CHANGED
|
@@ -12,7 +12,7 @@ function pac(adapter) {
|
|
|
12
12
|
throw new Error("This command requires --exchange pacifica");
|
|
13
13
|
return adapter;
|
|
14
14
|
}
|
|
15
|
-
export function registerTradeCommands(program, getAdapter, isJson, isDryRun = () => false) {
|
|
15
|
+
export function registerTradeCommands(program, getAdapter, isJson, isDryRun = () => false, getAdapterForExchange) {
|
|
16
16
|
/** Guard: if --dry-run is active, log the intended action and return without executing. */
|
|
17
17
|
function dryRunGuard(action, details) {
|
|
18
18
|
if (!isDryRun())
|
|
@@ -29,11 +29,11 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
29
29
|
console.log();
|
|
30
30
|
}
|
|
31
31
|
logExecution({
|
|
32
|
-
type: action.includes("limit") ? "limit_order" : action.includes("stop") ? "stop_order" : action.includes("cancel") ? "cancel" : "market_order",
|
|
32
|
+
type: action.includes("split") ? "split_order" : action.includes("limit") ? "limit_order" : action.includes("stop") ? "stop_order" : action.includes("cancel") ? "cancel" : "market_order",
|
|
33
33
|
exchange: details.exchange ?? "unknown",
|
|
34
34
|
symbol: (details.symbol ?? "").toUpperCase(),
|
|
35
35
|
side: details.side ?? "",
|
|
36
|
-
size: String(details.size ?? "0"),
|
|
36
|
+
size: String(details.size ?? details.totalUsd ?? "0"),
|
|
37
37
|
price: details.price,
|
|
38
38
|
status: "simulated",
|
|
39
39
|
dryRun: true,
|
|
@@ -48,12 +48,37 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
48
48
|
.option("-s, --slippage <pct>", "Slippage percent", "1")
|
|
49
49
|
.option("--reduce-only", "Reduce only order")
|
|
50
50
|
.option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick (reduces slippage)")
|
|
51
|
+
.option("--split", "Use orderbook-aware split execution for large orders")
|
|
52
|
+
.option("--max-slippage <pct>", "Max slippage per split slice (%)", "0.3")
|
|
51
53
|
.option("--client-id <id>", "Client order ID for idempotent tracking")
|
|
52
54
|
.option("--auto-id", "Auto-generate a client order ID")
|
|
53
55
|
.action(async (symbol, side, size, opts) => {
|
|
54
56
|
const s = side.toLowerCase();
|
|
55
57
|
if (s !== "buy" && s !== "sell")
|
|
56
58
|
errorAndExit("Side must be buy or sell");
|
|
59
|
+
const sym = symbol.toUpperCase();
|
|
60
|
+
// ── Split execution branch ──
|
|
61
|
+
if (opts.split) {
|
|
62
|
+
const adapter = await getAdapter();
|
|
63
|
+
if (dryRunGuard("split-order", { exchange: adapter.name, symbol: sym, side: s, size }))
|
|
64
|
+
return;
|
|
65
|
+
const { runSplitOrder } = await import("../strategies/split-order.js");
|
|
66
|
+
const markets = await adapter.getMarkets();
|
|
67
|
+
const market = markets.find(m => symbolMatch(m.symbol, sym));
|
|
68
|
+
const markPrice = market ? parseFloat(market.markPrice) : 0;
|
|
69
|
+
const notionalUsd = markPrice > 0 ? parseFloat(size) * markPrice : parseFloat(size);
|
|
70
|
+
const result = await runSplitOrder(adapter, {
|
|
71
|
+
symbol: sym,
|
|
72
|
+
side: s,
|
|
73
|
+
totalSizeUsd: notionalUsd,
|
|
74
|
+
maxSlippagePct: parseFloat(opts.maxSlippage ?? "0.3"),
|
|
75
|
+
}, isJson() ? () => { } : (msg) => console.log(chalk.gray(` ${msg}`)));
|
|
76
|
+
if (isJson())
|
|
77
|
+
return printJson(jsonOk(result));
|
|
78
|
+
const statusColor = result.status === "complete" ? chalk.green : result.status === "partial" ? chalk.yellow : chalk.red;
|
|
79
|
+
console.log(`\n ${statusColor(result.status.toUpperCase())} | ${result.slices.length} slices | $${formatUsd(result.filledUsd)} filled | slippage: ${result.totalSlippagePct.toFixed(3)}%\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
57
82
|
const clientId = opts.autoId ? generateClientId() : opts.clientId;
|
|
58
83
|
if (clientId && isOrderDuplicate(clientId)) {
|
|
59
84
|
if (isJson())
|
|
@@ -62,7 +87,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
62
87
|
return;
|
|
63
88
|
}
|
|
64
89
|
const adapter = await getAdapter();
|
|
65
|
-
if (dryRunGuard("market_order", { exchange: adapter.name, symbol:
|
|
90
|
+
if (dryRunGuard("market_order", { exchange: adapter.name, symbol: sym, side: s, size, smart: !!opts.smart }))
|
|
66
91
|
return;
|
|
67
92
|
if (clientId) {
|
|
68
93
|
logClientId({
|
|
@@ -186,6 +211,47 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
186
211
|
console.log(chalk.green(`\n Market SELL ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.${opts.smart ? " (smart)" : ""}\n`));
|
|
187
212
|
printJson(jsonOk(result));
|
|
188
213
|
});
|
|
214
|
+
// ── Split Order (orderbook-aware) ──
|
|
215
|
+
trade
|
|
216
|
+
.command("split <symbol> <side> <usd>")
|
|
217
|
+
.description("Orderbook-aware split execution — breaks large orders into slices based on depth")
|
|
218
|
+
.option("--max-slippage <pct>", "Max slippage per slice (%)", "0.3")
|
|
219
|
+
.option("--max-slices <n>", "Max number of slices", "10")
|
|
220
|
+
.option("--delay <ms>", "Delay between slices in ms", "1000")
|
|
221
|
+
.option("--min-slice <usd>", "Minimum slice size in USD", "100")
|
|
222
|
+
.action(async (symbol, side, usd, opts) => {
|
|
223
|
+
const sym = symbol.toUpperCase();
|
|
224
|
+
const orderSide = side.toLowerCase();
|
|
225
|
+
const totalUsd = parseFloat(usd);
|
|
226
|
+
if (!["buy", "sell"].includes(orderSide))
|
|
227
|
+
errorAndExit("Side must be 'buy' or 'sell'");
|
|
228
|
+
if (isNaN(totalUsd) || totalUsd <= 0)
|
|
229
|
+
errorAndExit("USD amount must be > 0");
|
|
230
|
+
const exchange = program.opts().exchange ?? "unknown";
|
|
231
|
+
if (dryRunGuard("split-order", { exchange, symbol: sym, side: orderSide, totalUsd, ...opts }))
|
|
232
|
+
return;
|
|
233
|
+
const adapter = await getAdapter();
|
|
234
|
+
const { runSplitOrder } = await import("../strategies/split-order.js");
|
|
235
|
+
const result = await runSplitOrder(adapter, {
|
|
236
|
+
symbol: sym,
|
|
237
|
+
side: orderSide,
|
|
238
|
+
totalSizeUsd: totalUsd,
|
|
239
|
+
maxSlippagePct: parseFloat(opts.maxSlippage),
|
|
240
|
+
maxSlices: parseInt(opts.maxSlices),
|
|
241
|
+
delayMs: parseInt(opts.delay),
|
|
242
|
+
minSliceUsd: parseFloat(opts.minSlice),
|
|
243
|
+
}, isJson() ? () => { } : (msg) => console.log(chalk.gray(` ${msg}`)));
|
|
244
|
+
if (isJson())
|
|
245
|
+
return printJson(jsonOk(result));
|
|
246
|
+
const statusColor = result.status === "complete" ? chalk.green
|
|
247
|
+
: result.status === "partial" ? chalk.yellow
|
|
248
|
+
: chalk.red;
|
|
249
|
+
console.log(`\n ${statusColor(result.status.toUpperCase())} | ${result.slices.length} slices`);
|
|
250
|
+
console.log(` Filled: $${formatUsd(result.filledUsd)} / $${formatUsd(result.requestedUsd)}`);
|
|
251
|
+
console.log(` Avg Price: $${formatUsd(result.avgPrice)}`);
|
|
252
|
+
console.log(` Slippage: ${result.totalSlippagePct.toFixed(3)}%`);
|
|
253
|
+
console.log(` Runtime: ${result.runtime.toFixed(1)}s\n`);
|
|
254
|
+
});
|
|
189
255
|
trade
|
|
190
256
|
.command("limit <symbol> <side> <price> <size>")
|
|
191
257
|
.description("Place a limit order")
|
|
@@ -622,120 +688,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
622
688
|
throw err;
|
|
623
689
|
}
|
|
624
690
|
});
|
|
625
|
-
//
|
|
626
|
-
trade
|
|
627
|
-
.command("grid <symbol>")
|
|
628
|
-
.description("Run grid trading bot (foreground or --background)")
|
|
629
|
-
.requiredOption("--upper <price>", "Upper price bound")
|
|
630
|
-
.requiredOption("--lower <price>", "Lower price bound")
|
|
631
|
-
.option("--grids <n>", "Number of grid lines", "10")
|
|
632
|
-
.option("--size <size>", "Total position size (base)", "0.1")
|
|
633
|
-
.option("--side <side>", "Grid bias: long, short, neutral", "neutral")
|
|
634
|
-
.option("--leverage <n>", "Leverage to set")
|
|
635
|
-
.option("--interval <sec>", "Check interval in seconds", "10")
|
|
636
|
-
.option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
|
|
637
|
-
.option("--trailing-stop <pct>", "Stop if equity drops by this % from peak")
|
|
638
|
-
.option("--background", "Run in background (tmux)")
|
|
639
|
-
.action(async (symbol, opts) => {
|
|
640
|
-
const exchange = (await getAdapter()).name;
|
|
641
|
-
const cliArgs = [
|
|
642
|
-
`-e`, exchange, symbol.toUpperCase(),
|
|
643
|
-
`--upper`, opts.upper, `--lower`, opts.lower,
|
|
644
|
-
`--grids`, opts.grids, `--size`, opts.size,
|
|
645
|
-
`--side`, opts.side, `--interval`, opts.interval,
|
|
646
|
-
`--max-runtime`, opts.maxRuntime,
|
|
647
|
-
...(opts.leverage ? [`--leverage`, opts.leverage] : []),
|
|
648
|
-
...(opts.trailingStop ? [`--trailing-stop`, opts.trailingStop] : []),
|
|
649
|
-
];
|
|
650
|
-
if (opts.background) {
|
|
651
|
-
const { startJob } = await import("../jobs.js");
|
|
652
|
-
const job = startJob({
|
|
653
|
-
strategy: "grid",
|
|
654
|
-
exchange,
|
|
655
|
-
params: { symbol: symbol.toUpperCase(), ...opts },
|
|
656
|
-
cliArgs,
|
|
657
|
-
});
|
|
658
|
-
if (isJson())
|
|
659
|
-
return printJson(jsonOk(job));
|
|
660
|
-
console.log(chalk.green(`\n Grid bot started in background.`));
|
|
661
|
-
console.log(` ID: ${chalk.white.bold(job.id)}`);
|
|
662
|
-
console.log(` Range: $${opts.lower} - $${opts.upper} | ${opts.grids} grids`);
|
|
663
|
-
console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
|
|
664
|
-
console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
// Foreground: delegate to `run grid`
|
|
668
|
-
const { runGrid } = await import("../strategies/grid.js");
|
|
669
|
-
const adapter = await getAdapter();
|
|
670
|
-
const log = (msg) => {
|
|
671
|
-
const ts = new Date().toLocaleTimeString();
|
|
672
|
-
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
673
|
-
};
|
|
674
|
-
await runGrid(adapter, {
|
|
675
|
-
symbol: symbol.toUpperCase(),
|
|
676
|
-
side: opts.side,
|
|
677
|
-
upperPrice: parseFloat(opts.upper),
|
|
678
|
-
lowerPrice: parseFloat(opts.lower),
|
|
679
|
-
grids: parseInt(opts.grids),
|
|
680
|
-
totalSize: parseFloat(opts.size),
|
|
681
|
-
leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
|
|
682
|
-
intervalSec: parseInt(opts.interval),
|
|
683
|
-
maxRuntime: parseInt(opts.maxRuntime),
|
|
684
|
-
trailingStop: opts.trailingStop ? parseFloat(opts.trailingStop) : undefined,
|
|
685
|
-
}, undefined, log);
|
|
686
|
-
});
|
|
687
|
-
// ── DCA (shortcut with --background) ──
|
|
688
|
-
trade
|
|
689
|
-
.command("dca <symbol> <side> <amount> <interval>")
|
|
690
|
-
.description("Run DCA strategy (foreground or --background)")
|
|
691
|
-
.option("--orders <n>", "Total number of orders (0 = unlimited)", "0")
|
|
692
|
-
.option("--price-limit <price>", "Stop buying above / selling below this price")
|
|
693
|
-
.option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
|
|
694
|
-
.option("--background", "Run in background (tmux)")
|
|
695
|
-
.action(async (symbol, side, amount, interval, opts) => {
|
|
696
|
-
const s = side.toLowerCase();
|
|
697
|
-
if (s !== "buy" && s !== "sell")
|
|
698
|
-
errorAndExit("Side must be buy or sell");
|
|
699
|
-
const exchange = (await getAdapter()).name;
|
|
700
|
-
const cliArgs = [
|
|
701
|
-
`-e`, exchange, symbol.toUpperCase(), s, amount, interval,
|
|
702
|
-
`--orders`, opts.orders, `--max-runtime`, opts.maxRuntime,
|
|
703
|
-
...(opts.priceLimit ? [`--price-limit`, opts.priceLimit] : []),
|
|
704
|
-
];
|
|
705
|
-
if (opts.background) {
|
|
706
|
-
const { startJob } = await import("../jobs.js");
|
|
707
|
-
const job = startJob({
|
|
708
|
-
strategy: "dca",
|
|
709
|
-
exchange,
|
|
710
|
-
params: { symbol: symbol.toUpperCase(), side: s, amount, interval, ...opts },
|
|
711
|
-
cliArgs,
|
|
712
|
-
});
|
|
713
|
-
if (isJson())
|
|
714
|
-
return printJson(jsonOk(job));
|
|
715
|
-
console.log(chalk.green(`\n DCA started in background.`));
|
|
716
|
-
console.log(` ID: ${chalk.white.bold(job.id)}`);
|
|
717
|
-
console.log(` ${s.toUpperCase()} ${amount} ${symbol.toUpperCase()} every ${interval}s`);
|
|
718
|
-
console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
|
|
719
|
-
console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
// Foreground
|
|
723
|
-
const { runDCA } = await import("../strategies/dca.js");
|
|
724
|
-
const adapter = await getAdapter();
|
|
725
|
-
const log = (msg) => {
|
|
726
|
-
const ts = new Date().toLocaleTimeString();
|
|
727
|
-
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
728
|
-
};
|
|
729
|
-
await runDCA(adapter, {
|
|
730
|
-
symbol: symbol.toUpperCase(),
|
|
731
|
-
side: s,
|
|
732
|
-
amountPerOrder: parseFloat(amount),
|
|
733
|
-
intervalSec: parseInt(interval),
|
|
734
|
-
totalOrders: parseInt(opts.orders),
|
|
735
|
-
priceLimit: opts.priceLimit ? parseFloat(opts.priceLimit) : undefined,
|
|
736
|
-
maxRuntime: parseInt(opts.maxRuntime),
|
|
737
|
-
}, undefined, log);
|
|
738
|
-
});
|
|
691
|
+
// grid/dca moved to 'perp bot grid' / 'perp bot dca'
|
|
739
692
|
// ── Position Management Shortcuts ──
|
|
740
693
|
trade
|
|
741
694
|
.command("close-all")
|
|
@@ -1124,147 +1077,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
1124
1077
|
}
|
|
1125
1078
|
console.log();
|
|
1126
1079
|
});
|
|
1127
|
-
//
|
|
1128
|
-
trade
|
|
1129
|
-
.command("trailing-stop <symbol>")
|
|
1130
|
-
.description("Client-side trailing stop that monitors price and closes position when price drops by X% from peak")
|
|
1131
|
-
.requiredOption("--trail <pct>", "Trail percentage (e.g., 3 = close when price drops 3% from high)")
|
|
1132
|
-
.option("--interval <sec>", "Check interval in seconds", "5")
|
|
1133
|
-
.option("--activation <price>", "Only start trailing after price reaches this level")
|
|
1134
|
-
.option("--background", "Run in background (tmux)")
|
|
1135
|
-
.action(async (symbol, opts) => {
|
|
1136
|
-
const sym = symbol.toUpperCase();
|
|
1137
|
-
const trailPct = parseFloat(opts.trail);
|
|
1138
|
-
const intervalSec = parseInt(opts.interval);
|
|
1139
|
-
const activationPrice = opts.activation ? parseFloat(opts.activation) : undefined;
|
|
1140
|
-
if (isNaN(trailPct) || trailPct <= 0)
|
|
1141
|
-
errorAndExit("Trail percentage must be > 0");
|
|
1142
|
-
const exchange = (await getAdapter()).name;
|
|
1143
|
-
// --background → run via tmux
|
|
1144
|
-
if (opts.background) {
|
|
1145
|
-
const { startJob } = await import("../jobs.js");
|
|
1146
|
-
const cliArgs = [
|
|
1147
|
-
`-e`, exchange, sym,
|
|
1148
|
-
`--trail`, opts.trail,
|
|
1149
|
-
`--interval`, opts.interval,
|
|
1150
|
-
...(opts.activation ? [`--activation`, opts.activation] : []),
|
|
1151
|
-
];
|
|
1152
|
-
const job = startJob({
|
|
1153
|
-
strategy: "trailing-stop",
|
|
1154
|
-
exchange,
|
|
1155
|
-
params: { symbol: sym, trail: trailPct, interval: intervalSec, activation: activationPrice },
|
|
1156
|
-
cliArgs,
|
|
1157
|
-
});
|
|
1158
|
-
if (isJson())
|
|
1159
|
-
return printJson(jsonOk(job));
|
|
1160
|
-
console.log(chalk.green(`\n Trailing stop started in background.`));
|
|
1161
|
-
console.log(` ID: ${chalk.white.bold(job.id)}`);
|
|
1162
|
-
console.log(` Trail: ${trailPct}%${activationPrice ? ` | Activation: $${activationPrice}` : ""}`);
|
|
1163
|
-
console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
|
|
1164
|
-
console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
// Foreground: run trailing stop loop
|
|
1168
|
-
const adapter = await getAdapter();
|
|
1169
|
-
// Auto-detect position side
|
|
1170
|
-
const positions = await adapter.getPositions();
|
|
1171
|
-
const pos = positions.find(p => symbolMatch(p.symbol, sym));
|
|
1172
|
-
if (!pos)
|
|
1173
|
-
errorAndExit(`No open position for ${sym}. Open a position first.`);
|
|
1174
|
-
const positionSide = pos.side; // "long" or "short"
|
|
1175
|
-
const closeSide = positionSide === "long" ? "sell" : "buy";
|
|
1176
|
-
const posSize = pos.size;
|
|
1177
|
-
console.log(chalk.cyan(`\n Trailing Stop for ${sym} (${positionSide} ${posSize})`));
|
|
1178
|
-
console.log(chalk.cyan(` Trail: ${trailPct}% | Interval: ${intervalSec}s${activationPrice ? ` | Activation: $${activationPrice}` : ""}`));
|
|
1179
|
-
console.log(chalk.gray(` Press Ctrl+C to cancel.\n`));
|
|
1180
|
-
let peakPrice = 0;
|
|
1181
|
-
let activated = !activationPrice; // if no activation price, start immediately
|
|
1182
|
-
let running = true;
|
|
1183
|
-
const cleanup = () => { running = false; };
|
|
1184
|
-
process.on("SIGINT", cleanup);
|
|
1185
|
-
process.on("SIGTERM", cleanup);
|
|
1186
|
-
try {
|
|
1187
|
-
while (running) {
|
|
1188
|
-
const markets = await adapter.getMarkets();
|
|
1189
|
-
const market = markets.find(m => symbolMatch(m.symbol, sym));
|
|
1190
|
-
if (!market) {
|
|
1191
|
-
console.log(chalk.yellow(` Market data for ${sym} not found, retrying...`));
|
|
1192
|
-
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
const currentPrice = parseFloat(market.markPrice);
|
|
1196
|
-
// Check activation
|
|
1197
|
-
if (!activated && activationPrice) {
|
|
1198
|
-
if (positionSide === "long" && currentPrice >= activationPrice) {
|
|
1199
|
-
activated = true;
|
|
1200
|
-
console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (>= $${activationPrice})`));
|
|
1201
|
-
}
|
|
1202
|
-
else if (positionSide === "short" && currentPrice <= activationPrice) {
|
|
1203
|
-
activated = true;
|
|
1204
|
-
console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (<= $${activationPrice})`));
|
|
1205
|
-
}
|
|
1206
|
-
else {
|
|
1207
|
-
const ts = new Date().toLocaleTimeString();
|
|
1208
|
-
console.log(chalk.gray(` ${ts} | $${currentPrice.toFixed(2)} | Waiting for activation ($${activationPrice})...`));
|
|
1209
|
-
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
// Update peak
|
|
1214
|
-
if (positionSide === "long") {
|
|
1215
|
-
if (currentPrice > peakPrice)
|
|
1216
|
-
peakPrice = currentPrice;
|
|
1217
|
-
const dropPct = ((peakPrice - currentPrice) / peakPrice) * 100;
|
|
1218
|
-
const ts = new Date().toLocaleTimeString();
|
|
1219
|
-
console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Peak: $${peakPrice.toFixed(2)} | Drop: ${dropPct.toFixed(2)}%`));
|
|
1220
|
-
if (dropPct >= trailPct) {
|
|
1221
|
-
console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price dropped ${dropPct.toFixed(2)}% from peak $${peakPrice.toFixed(2)}`));
|
|
1222
|
-
console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
|
|
1223
|
-
const result = await adapter.marketOrder(sym, closeSide, posSize);
|
|
1224
|
-
logExecution({
|
|
1225
|
-
type: "market_order", exchange: adapter.name, symbol: sym,
|
|
1226
|
-
side: closeSide, size: posSize, status: "success", dryRun: false,
|
|
1227
|
-
meta: { action: "trailing-stop", trailPct, peakPrice, triggerPrice: currentPrice },
|
|
1228
|
-
});
|
|
1229
|
-
if (isJson())
|
|
1230
|
-
return printJson(jsonOk({ triggered: true, peakPrice, triggerPrice: currentPrice, dropPct, result }));
|
|
1231
|
-
console.log(chalk.green(` Position closed. Peak: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
else {
|
|
1236
|
-
// Short position: track lowest price, trigger when price rises
|
|
1237
|
-
if (peakPrice === 0 || currentPrice < peakPrice)
|
|
1238
|
-
peakPrice = currentPrice;
|
|
1239
|
-
const risePct = ((currentPrice - peakPrice) / peakPrice) * 100;
|
|
1240
|
-
const ts = new Date().toLocaleTimeString();
|
|
1241
|
-
console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Trough: $${peakPrice.toFixed(2)} | Rise: ${risePct.toFixed(2)}%`));
|
|
1242
|
-
if (risePct >= trailPct) {
|
|
1243
|
-
console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price rose ${risePct.toFixed(2)}% from trough $${peakPrice.toFixed(2)}`));
|
|
1244
|
-
console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
|
|
1245
|
-
const result = await adapter.marketOrder(sym, closeSide, posSize);
|
|
1246
|
-
logExecution({
|
|
1247
|
-
type: "market_order", exchange: adapter.name, symbol: sym,
|
|
1248
|
-
side: closeSide, size: posSize, status: "success", dryRun: false,
|
|
1249
|
-
meta: { action: "trailing-stop", trailPct, troughPrice: peakPrice, triggerPrice: currentPrice },
|
|
1250
|
-
});
|
|
1251
|
-
if (isJson())
|
|
1252
|
-
return printJson(jsonOk({ triggered: true, troughPrice: peakPrice, triggerPrice: currentPrice, risePct, result }));
|
|
1253
|
-
console.log(chalk.green(` Position closed. Trough: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
finally {
|
|
1261
|
-
process.removeListener("SIGINT", cleanup);
|
|
1262
|
-
process.removeListener("SIGTERM", cleanup);
|
|
1263
|
-
}
|
|
1264
|
-
if (isJson())
|
|
1265
|
-
return printJson(jsonOk({ triggered: false, reason: "cancelled" }));
|
|
1266
|
-
console.log(chalk.yellow(`\n Trailing stop cancelled.\n`));
|
|
1267
|
-
});
|
|
1080
|
+
// grid/dca/trailing-stop moved to 'perp bot'
|
|
1268
1081
|
// ── PnL Tracker ──
|
|
1269
1082
|
trade
|
|
1270
1083
|
.command("pnl-track")
|
|
@@ -1345,4 +1158,173 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
1345
1158
|
}
|
|
1346
1159
|
console.log(chalk.yellow(`\n PnL tracker stopped.\n`));
|
|
1347
1160
|
});
|
|
1161
|
+
// ── Multi-leg orders ──────────────────────────────────────────────────
|
|
1162
|
+
if (getAdapterForExchange) {
|
|
1163
|
+
const tradeCmd = program.commands.find(c => c.name() === "trade");
|
|
1164
|
+
if (tradeCmd) {
|
|
1165
|
+
registerMultiAction(tradeCmd, getAdapterForExchange, isJson);
|
|
1166
|
+
}
|
|
1167
|
+
// Keep deprecated top-level 'multi' alias (hidden from help)
|
|
1168
|
+
registerMultiAction(program, getAdapterForExchange, isJson, true);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function parseMultiLeg(spec) {
|
|
1172
|
+
const parts = spec.split(":");
|
|
1173
|
+
if (parts.length !== 4) {
|
|
1174
|
+
throw new Error(`Invalid leg format "${spec}" — expected exchange:symbol:side:size`);
|
|
1175
|
+
}
|
|
1176
|
+
const [exchange, symbol, side, size] = parts;
|
|
1177
|
+
// Normalize exchange aliases
|
|
1178
|
+
const exMap = {
|
|
1179
|
+
hl: "hyperliquid", pac: "pacifica", lit: "lighter",
|
|
1180
|
+
hyperliquid: "hyperliquid", pacifica: "pacifica", lighter: "lighter",
|
|
1181
|
+
};
|
|
1182
|
+
const normalizedExchange = exMap[exchange.toLowerCase()];
|
|
1183
|
+
if (!normalizedExchange) {
|
|
1184
|
+
throw new Error(`Unknown exchange "${exchange}" in leg spec`);
|
|
1185
|
+
}
|
|
1186
|
+
if (side !== "buy" && side !== "sell") {
|
|
1187
|
+
throw new Error(`Invalid side "${side}" — must be "buy" or "sell"`);
|
|
1188
|
+
}
|
|
1189
|
+
if (isNaN(Number(size)) || Number(size) <= 0) {
|
|
1190
|
+
throw new Error(`Invalid size "${size}" — must be a positive number`);
|
|
1191
|
+
}
|
|
1192
|
+
return { exchange: normalizedExchange, symbol: symbol.toUpperCase(), side, size };
|
|
1193
|
+
}
|
|
1194
|
+
function registerMultiAction(parent, getAdapterForExchange, isJson, deprecated = false) {
|
|
1195
|
+
const cmd = parent
|
|
1196
|
+
.command("multi <legs...>")
|
|
1197
|
+
.description(deprecated ? "Use 'perp trade multi'" : "Execute multi-leg orders (exchange:symbol:side:size)");
|
|
1198
|
+
if (deprecated)
|
|
1199
|
+
cmd._hidden = true;
|
|
1200
|
+
cmd
|
|
1201
|
+
.option("--smart", "Use smart order (IOC limit + fallback) for each leg")
|
|
1202
|
+
.option("--rollback", "Auto-rollback filled legs if any leg fails", true)
|
|
1203
|
+
.option("--no-rollback", "Disable auto-rollback")
|
|
1204
|
+
.option("--timeout <ms>", "Per-leg timeout in milliseconds", "30000")
|
|
1205
|
+
.action(async (legSpecs, opts) => {
|
|
1206
|
+
await withJsonErrors(isJson(), async () => {
|
|
1207
|
+
// Parse legs
|
|
1208
|
+
const legs = legSpecs.map(parseMultiLeg);
|
|
1209
|
+
if (legs.length < 2) {
|
|
1210
|
+
const err = "Multi-leg requires at least 2 legs";
|
|
1211
|
+
if (isJson()) {
|
|
1212
|
+
printJson(jsonError("INVALID_ARGS", err));
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
throw new Error(err);
|
|
1216
|
+
}
|
|
1217
|
+
if (!isJson()) {
|
|
1218
|
+
console.log(chalk.cyan.bold("Multi-Leg Order\n"));
|
|
1219
|
+
for (const l of legs) {
|
|
1220
|
+
const color = l.side === "buy" ? chalk.green : chalk.red;
|
|
1221
|
+
console.log(` ${l.exchange.padEnd(14)} ${color(l.side.toUpperCase())} ${l.symbol} x ${l.size}`);
|
|
1222
|
+
}
|
|
1223
|
+
console.log();
|
|
1224
|
+
}
|
|
1225
|
+
// Get adapters (deduplicated)
|
|
1226
|
+
const adapters = new Map();
|
|
1227
|
+
for (const l of legs) {
|
|
1228
|
+
if (!adapters.has(l.exchange)) {
|
|
1229
|
+
adapters.set(l.exchange, await getAdapterForExchange(l.exchange));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Execute all legs simultaneously
|
|
1233
|
+
const timeoutMs = parseInt(opts.timeout);
|
|
1234
|
+
const results = await Promise.allSettled(legs.map(async (leg) => {
|
|
1235
|
+
const adapter = adapters.get(leg.exchange);
|
|
1236
|
+
const orderPromise = opts.smart
|
|
1237
|
+
? smartOrder(adapter, leg.symbol, leg.side, leg.size).then(r => r.result)
|
|
1238
|
+
: adapter.marketOrder(leg.symbol, leg.side, leg.size);
|
|
1239
|
+
// Apply timeout
|
|
1240
|
+
const result = await Promise.race([
|
|
1241
|
+
orderPromise,
|
|
1242
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Leg timeout")), timeoutMs)),
|
|
1243
|
+
]);
|
|
1244
|
+
// Log execution
|
|
1245
|
+
logExecution({
|
|
1246
|
+
type: "multi_leg",
|
|
1247
|
+
exchange: leg.exchange,
|
|
1248
|
+
symbol: leg.symbol,
|
|
1249
|
+
side: leg.side,
|
|
1250
|
+
size: leg.size,
|
|
1251
|
+
status: "success",
|
|
1252
|
+
dryRun: false,
|
|
1253
|
+
meta: { result, smart: opts.smart },
|
|
1254
|
+
});
|
|
1255
|
+
return { leg, status: "filled", result };
|
|
1256
|
+
}));
|
|
1257
|
+
// Process results
|
|
1258
|
+
const legResults = results.map((r, i) => {
|
|
1259
|
+
if (r.status === "fulfilled")
|
|
1260
|
+
return r.value;
|
|
1261
|
+
return {
|
|
1262
|
+
leg: legs[i],
|
|
1263
|
+
status: "failed",
|
|
1264
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason),
|
|
1265
|
+
};
|
|
1266
|
+
});
|
|
1267
|
+
const filled = legResults.filter(r => r.status === "filled");
|
|
1268
|
+
const failed = legResults.filter(r => r.status === "failed");
|
|
1269
|
+
// Rollback if partial fill and rollback enabled
|
|
1270
|
+
if (failed.length > 0 && filled.length > 0 && opts.rollback) {
|
|
1271
|
+
if (!isJson()) {
|
|
1272
|
+
console.log(chalk.yellow(`\n ${failed.length} leg(s) failed — rolling back ${filled.length} filled leg(s)...\n`));
|
|
1273
|
+
}
|
|
1274
|
+
for (const fr of filled) {
|
|
1275
|
+
const adapter = adapters.get(fr.leg.exchange);
|
|
1276
|
+
const reverseSide = fr.leg.side === "buy" ? "sell" : "buy";
|
|
1277
|
+
try {
|
|
1278
|
+
await adapter.marketOrder(fr.leg.symbol, reverseSide, fr.leg.size);
|
|
1279
|
+
fr.status = "rolled_back";
|
|
1280
|
+
logExecution({
|
|
1281
|
+
type: "multi_leg_rollback",
|
|
1282
|
+
exchange: fr.leg.exchange,
|
|
1283
|
+
symbol: fr.leg.symbol,
|
|
1284
|
+
side: reverseSide,
|
|
1285
|
+
size: fr.leg.size,
|
|
1286
|
+
status: "success",
|
|
1287
|
+
dryRun: false,
|
|
1288
|
+
});
|
|
1289
|
+
if (!isJson()) {
|
|
1290
|
+
console.log(chalk.gray(` Rolled back: ${fr.leg.exchange} ${reverseSide} ${fr.leg.symbol} x ${fr.leg.size}`));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
catch (err) {
|
|
1294
|
+
if (!isJson()) {
|
|
1295
|
+
console.log(chalk.red(` Rollback failed: ${fr.leg.exchange} ${fr.leg.symbol} — ${err instanceof Error ? err.message : err}`));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (isJson()) {
|
|
1301
|
+
printJson(jsonOk({
|
|
1302
|
+
legs: legResults.map(r => ({
|
|
1303
|
+
exchange: r.leg.exchange,
|
|
1304
|
+
symbol: r.leg.symbol,
|
|
1305
|
+
side: r.leg.side,
|
|
1306
|
+
size: r.leg.size,
|
|
1307
|
+
status: r.status,
|
|
1308
|
+
error: r.error,
|
|
1309
|
+
})),
|
|
1310
|
+
summary: {
|
|
1311
|
+
total: legs.length,
|
|
1312
|
+
filled: legResults.filter(r => r.status === "filled").length,
|
|
1313
|
+
failed: failed.length,
|
|
1314
|
+
rolledBack: legResults.filter(r => r.status === "rolled_back").length,
|
|
1315
|
+
},
|
|
1316
|
+
}));
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
console.log(chalk.cyan.bold("\nResults:\n"));
|
|
1320
|
+
for (const r of legResults) {
|
|
1321
|
+
const icon = r.status === "filled" ? chalk.green("OK") :
|
|
1322
|
+
r.status === "rolled_back" ? chalk.yellow("ROLLBACK") :
|
|
1323
|
+
chalk.red("FAIL");
|
|
1324
|
+
console.log(` ${icon} ${r.leg.exchange.padEnd(14)} ${r.leg.side} ${r.leg.symbol} x ${r.leg.size}${r.error ? ` — ${r.error}` : ""}`);
|
|
1325
|
+
}
|
|
1326
|
+
console.log(`\n Total: ${legs.length} Filled: ${filled.length} Failed: ${failed.length} Rolled back: ${legResults.filter(r => r.status === "rolled_back").length}`);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1348
1330
|
}
|
|
@@ -11,4 +11,8 @@ export declare function getWalletSetupStatus(): {
|
|
|
11
11
|
};
|
|
12
12
|
/** Exported so config.ts can resolve the active wallet key */
|
|
13
13
|
export declare function getActiveWalletKey(exchange: string): string | null;
|
|
14
|
+
/** Get wallet key by name. Returns null if wallet doesn't exist. */
|
|
15
|
+
export declare function getWalletKeyByName(name: string): string | null;
|
|
16
|
+
/** Get all wallet names. */
|
|
17
|
+
export declare function getWalletNames(): string[];
|
|
14
18
|
export declare function registerWalletCommands(program: Command, isJson: () => boolean): void;
|
package/dist/commands/wallet.js
CHANGED
|
@@ -42,6 +42,16 @@ export function getActiveWalletKey(exchange) {
|
|
|
42
42
|
return null;
|
|
43
43
|
return store.wallets[name]?.privateKey ?? null;
|
|
44
44
|
}
|
|
45
|
+
/** Get wallet key by name. Returns null if wallet doesn't exist. */
|
|
46
|
+
export function getWalletKeyByName(name) {
|
|
47
|
+
const store = loadStore();
|
|
48
|
+
return store.wallets[name]?.privateKey ?? null;
|
|
49
|
+
}
|
|
50
|
+
/** Get all wallet names. */
|
|
51
|
+
export function getWalletNames() {
|
|
52
|
+
const store = loadStore();
|
|
53
|
+
return Object.keys(store.wallets);
|
|
54
|
+
}
|
|
45
55
|
async function getSolanaBalances(address, isTestnet) {
|
|
46
56
|
const { Connection, PublicKey } = await import("@solana/web3.js");
|
|
47
57
|
const rpcUrl = isTestnet
|
package/dist/config.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type Exchange = "pacifica" | "hyperliquid" | "lighter";
|
|
|
4
4
|
* Try to load a private key without throwing if none is found.
|
|
5
5
|
* Returns null if no key is configured.
|
|
6
6
|
*/
|
|
7
|
-
export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string): Promise<string | null>;
|
|
8
|
-
export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string): Promise<string>;
|
|
7
|
+
export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string | null>;
|
|
8
|
+
export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string>;
|
|
9
9
|
export declare function parseSolanaKeypair(input: string): Keypair;
|
|
10
10
|
export declare function isEvmPrivateKey(input: string): boolean;
|
package/dist/config.js
CHANGED
|
@@ -7,15 +7,23 @@ import { existsSync, readFileSync } from "fs";
|
|
|
7
7
|
* Try to load a private key without throwing if none is found.
|
|
8
8
|
* Returns null if no key is configured.
|
|
9
9
|
*/
|
|
10
|
-
export async function tryLoadPrivateKey(exchange, pkOverride) {
|
|
10
|
+
export async function tryLoadPrivateKey(exchange, pkOverride, walletName) {
|
|
11
11
|
try {
|
|
12
|
-
return await loadPrivateKey(exchange, pkOverride);
|
|
12
|
+
return await loadPrivateKey(exchange, pkOverride, walletName);
|
|
13
13
|
}
|
|
14
14
|
catch {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export async function loadPrivateKey(exchange, pkOverride) {
|
|
18
|
+
export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
19
|
+
// 0. Wallet name override (--wallet flag)
|
|
20
|
+
if (walletName) {
|
|
21
|
+
const { getWalletKeyByName } = await import("./commands/wallet.js");
|
|
22
|
+
const key = getWalletKeyByName(walletName);
|
|
23
|
+
if (!key)
|
|
24
|
+
throw new Error(`Wallet "${walletName}" not found. Run: perp wallet list`);
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
19
27
|
// 1. CLI flag
|
|
20
28
|
if (pkOverride)
|
|
21
29
|
return pkOverride;
|
|
@@ -15,6 +15,7 @@ export declare class HyperliquidSpotAdapter implements SpotAdapter {
|
|
|
15
15
|
private _spotAssetReverse;
|
|
16
16
|
private _spotDecimals;
|
|
17
17
|
private _initialized;
|
|
18
|
+
private _cachedMetaCtx;
|
|
18
19
|
constructor(hlAdapter: HyperliquidAdapter);
|
|
19
20
|
/** Load spot metadata from the Hyperliquid spot API */
|
|
20
21
|
init(): Promise<void>;
|