perp-cli 0.7.9 → 0.7.11
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/dist/bot/engine.js
CHANGED
|
@@ -365,6 +365,14 @@ async function executeActions(adapter, symbol, actions, log, dryRun = false) {
|
|
|
365
365
|
}
|
|
366
366
|
// ── Enriched snapshot ──
|
|
367
367
|
async function getEnrichedSnapshot(adapter, symbol) {
|
|
368
|
+
// Multi-symbol strategies use "ALL" — skip market-specific data
|
|
369
|
+
if (symbol.toUpperCase() === "ALL") {
|
|
370
|
+
return {
|
|
371
|
+
price: 0, high24h: 0, low24h: 0, volume24h: 0,
|
|
372
|
+
fundingRate: 0, volatility24h: 0, rsi: NaN, spreadPct: 0,
|
|
373
|
+
klines: [], orderbook: { bids: [], asks: [] }, openInterest: "0",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
368
376
|
const base = await getMarketSnapshot(adapter, symbol);
|
|
369
377
|
const [orderbook, klines] = await Promise.all([
|
|
370
378
|
adapter.getOrderbook(symbol).catch(() => ({ bids: [], asks: [] })),
|
|
@@ -141,8 +141,30 @@ export class FundingArbStrategy {
|
|
|
141
141
|
if (longAdapter && shortAdapter) {
|
|
142
142
|
const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
|
|
143
143
|
if (price > 0) {
|
|
144
|
+
// Balance check — reduce size to fit available balance
|
|
145
|
+
let targetSizeUsd = params.size_usd;
|
|
146
|
+
try {
|
|
147
|
+
const [longBal, shortBal] = await Promise.all([
|
|
148
|
+
longAdapter.getBalance(),
|
|
149
|
+
shortAdapter.getBalance(),
|
|
150
|
+
]);
|
|
151
|
+
const longAvail = parseFloat(longBal.available);
|
|
152
|
+
const shortAvail = parseFloat(shortBal.available);
|
|
153
|
+
const minAvail = Math.min(longAvail, shortAvail);
|
|
154
|
+
// Use at most 80% of available balance per leg
|
|
155
|
+
const maxAffordable = minAvail * 0.8;
|
|
156
|
+
if (maxAffordable < 10) {
|
|
157
|
+
ctx.log(` [ARB] Skip ${best.symbol}: insufficient balance ($${minAvail.toFixed(2)} available, need $10+ per leg)`);
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
if (targetSizeUsd > maxAffordable) {
|
|
161
|
+
ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordable.toFixed(0)} (limited by balance $${minAvail.toFixed(2)})`);
|
|
162
|
+
targetSizeUsd = Math.floor(maxAffordable);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* non-critical, proceed with original size */ }
|
|
144
166
|
// Liquidity check
|
|
145
|
-
const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol,
|
|
167
|
+
const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
|
|
146
168
|
if (!liq.viable)
|
|
147
169
|
return [];
|
|
148
170
|
// Compute matched size
|
|
@@ -56,10 +56,10 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
|
|
|
56
56
|
exit();
|
|
57
57
|
}, [onQuit, exit]);
|
|
58
58
|
useInput((input, key) => {
|
|
59
|
-
if (input === "q" || (key.ctrl && input === "c")) {
|
|
59
|
+
if (input === "q" || input === "Q" || (key.ctrl && input === "c")) {
|
|
60
60
|
handleQuit();
|
|
61
61
|
}
|
|
62
|
-
if (input === "p") {
|
|
62
|
+
if (input === "p" || input === "P") {
|
|
63
63
|
setPaused((v) => !v);
|
|
64
64
|
onPause();
|
|
65
65
|
}
|
|
@@ -69,7 +69,8 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
|
|
|
69
69
|
const bidOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "buy");
|
|
70
70
|
const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
|
|
71
71
|
const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
|
|
72
|
-
|
|
72
|
+
const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
|
|
73
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", (state.exchangeBalances?.reduce((s, e) => s + parseFloat(e.equity), 0) ?? state.equity).toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsxs(Text, { color: "white", children: [state.volatility24h.toFixed(1), "%"] })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.equity.toFixed(2)] }), _jsxs(Text, { color: "gray", children: [" (peak: $", state.peakEquity.toFixed(2), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsxs(Text, { bold: true, color: drawdown > 0 ? "red" : "green", children: ["$", drawdown.toFixed(2), " (", drawdownPct.toFixed(1), "%)"] })] }), _jsxs(Text, { children: ["Fills: ", _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), _jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsxs(Text, { color: "gray", children: ["$", parseFloat(eb.available).toFixed(2)] })] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), state.positions.length === 0 ? (_jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
|
|
73
74
|
const upnl = parseFloat(p.unrealizedPnl);
|
|
74
75
|
return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsxs(Text, { color: pnlColor(upnl), children: ["$", upnl.toFixed(2)] })] }, i));
|
|
75
76
|
})] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Strategy State" }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [Object.entries(state.strategyState).slice(0, 12).map(([k, v]) => (_jsxs(Box, { marginRight: 2, children: [_jsxs(Text, { color: "gray", children: [k, ": "] }), _jsx(Text, { color: "white", children: String(v) })] }, k))), Object.keys(state.strategyState).length === 0 && (_jsx(Text, { color: "gray", children: " --" }))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: MAX_LOG_LINES + 2, children: [_jsx(Text, { color: "cyan", bold: true, children: "Log" }), logs.length === 0 ? (_jsx(Text, { color: "gray", children: " Waiting for events..." })) : (logs.map((entry, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "gray", children: [entry.time, " "] }), _jsx(Text, { children: colorizeLog(entry.message) })] }, i))))] }), _jsx(Box, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: _jsxs(Text, { color: "gray", children: ["[", _jsx(Text, { color: "white", bold: true, children: "Q" }), "]uit [", _jsx(Text, { color: "white", bold: true, children: "P" }), "]", paused ? "resume" : "ause"] }) })] }));
|
package/dist/commands/bot.js
CHANGED
|
@@ -430,12 +430,26 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
|
|
|
430
430
|
});
|
|
431
431
|
// ── bot run <strategy> <symbol> ──
|
|
432
432
|
bot
|
|
433
|
-
.command("run
|
|
433
|
+
.command("run [strategy] [symbol]")
|
|
434
434
|
.description("Run a strategy (use 'perp bot list-strategies' to see all). Symbol optional for multi-symbol strategies.")
|
|
435
435
|
.option("--config <path>", "YAML/JSON config file")
|
|
436
436
|
.option("--headless", "Run without TUI dashboard")
|
|
437
437
|
.option("--param <key=value>", "Strategy parameter (repeatable)", (val, acc) => [...acc, val], [])
|
|
438
438
|
.action(async (strategyName, symbol, opts) => {
|
|
439
|
+
if (!strategyName) {
|
|
440
|
+
await import("../bot/engine.js");
|
|
441
|
+
const { listStrategies: listStrats } = await import("../bot/strategy-registry.js");
|
|
442
|
+
const available = listStrats();
|
|
443
|
+
if (isJson()) {
|
|
444
|
+
printJson(jsonOk({ strategies: available }));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
console.log(chalk.cyan.bold("\n Available Strategies:\n"));
|
|
448
|
+
for (const s of available)
|
|
449
|
+
console.log(` ${chalk.green(s)}`);
|
|
450
|
+
console.log(chalk.gray(`\n Usage: perp bot run <strategy> [symbol]\n`));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
439
453
|
const sym = symbol?.toUpperCase() || "ALL";
|
|
440
454
|
// Strategies that require an explicit symbol
|
|
441
455
|
const symbolRequiredStrategies = [
|