perp-cli 0.7.1 → 0.7.2

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.
@@ -56,6 +56,7 @@ export interface BotConfig {
56
56
  monitor_interval_sec: number;
57
57
  }
58
58
  export declare function loadBotConfig(pathOrInline: string): BotConfig;
59
+ export declare function parseStrategy(type: string, raw: Record<string, unknown>): StrategyParams;
59
60
  export declare function quickGridConfig(opts: {
60
61
  exchange: string;
61
62
  symbol: string;
@@ -53,7 +53,7 @@ function parseBotConfig(raw) {
53
53
  };
54
54
  return { name, exchange, symbol, strategy, entry_conditions: entryConds, exit_conditions: exitConds, risk, monitor_interval_sec: monitorInterval };
55
55
  }
56
- function parseStrategy(type, raw) {
56
+ export function parseStrategy(type, raw) {
57
57
  switch (type) {
58
58
  case "grid":
59
59
  return {
@@ -247,9 +247,12 @@ mode = "headless") {
247
247
  }
248
248
  }
249
249
  // ── Emit state update for TUI / JSON ──
250
- const positions = await fetchPositions(adapter, config.symbol);
251
- const openOrders = await fetchOpenOrders(adapter, config.symbol);
252
- const tuiState = buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx);
250
+ const [positions, openOrders, exchangeBalances] = await Promise.all([
251
+ extraAdapters ? fetchAllPositions(adapter, config.symbol, extraAdapters) : fetchPositions(adapter, config.symbol, adapter.name),
252
+ fetchOpenOrders(adapter, config.symbol),
253
+ extraAdapters ? fetchExchangeBalances(adapter, extraAdapters) : Promise.resolve(undefined),
254
+ ]);
255
+ const tuiState = buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx, exchangeBalances);
253
256
  if (mode === "tui") {
254
257
  tuiEmitter.emitState(tuiState);
255
258
  }
@@ -390,11 +393,12 @@ function sleep(ms) {
390
393
  return new Promise(r => setTimeout(r, ms));
391
394
  }
392
395
  // ── TUI helpers ──
393
- async function fetchPositions(adapter, symbol) {
396
+ async function fetchPositions(adapter, symbol, exchangeName) {
394
397
  try {
395
398
  const positions = await adapter.getPositions();
399
+ const isAll = symbol.toUpperCase() === "ALL";
396
400
  return positions
397
- .filter(p => p.symbol.toUpperCase().includes(symbol.toUpperCase()) && parseFloat(p.size) !== 0)
401
+ .filter(p => (isAll || p.symbol.toUpperCase().includes(symbol.toUpperCase())) && parseFloat(p.size) !== 0)
398
402
  .map(p => ({
399
403
  symbol: p.symbol,
400
404
  side: p.side,
@@ -402,12 +406,42 @@ async function fetchPositions(adapter, symbol) {
402
406
  entryPrice: p.entryPrice,
403
407
  markPrice: p.markPrice,
404
408
  unrealizedPnl: p.unrealizedPnl,
409
+ exchange: exchangeName ?? adapter.name,
405
410
  }));
406
411
  }
407
412
  catch {
408
413
  return [];
409
414
  }
410
415
  }
416
+ async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
417
+ const adapters = [[primaryAdapter.name, primaryAdapter]];
418
+ if (extraAdapters) {
419
+ for (const [name, a] of extraAdapters)
420
+ adapters.push([name, a]);
421
+ }
422
+ const results = [];
423
+ await Promise.all(adapters.map(async ([name, a]) => {
424
+ try {
425
+ const bal = await a.getBalance();
426
+ results.push({ exchange: name, equity: bal.equity, available: bal.available });
427
+ }
428
+ catch { /* skip */ }
429
+ }));
430
+ return results;
431
+ }
432
+ async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
433
+ const adapters = [[primaryAdapter.name, primaryAdapter]];
434
+ if (extraAdapters) {
435
+ for (const [name, a] of extraAdapters)
436
+ adapters.push([name, a]);
437
+ }
438
+ const all = [];
439
+ await Promise.all(adapters.map(async ([name, a]) => {
440
+ const pos = await fetchPositions(a, symbol, name);
441
+ all.push(pos);
442
+ }));
443
+ return all.flat();
444
+ }
411
445
  async function fetchOpenOrders(adapter, symbol) {
412
446
  try {
413
447
  const orders = await adapter.getOpenOrders();
@@ -425,7 +459,7 @@ async function fetchOpenOrders(adapter, symbol) {
425
459
  return [];
426
460
  }
427
461
  }
428
- function buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx) {
462
+ function buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx, exchangeBalances) {
429
463
  // Extract serializable strategy state from the Map
430
464
  const strategyState = {};
431
465
  for (const [k, v] of strategyCtx.state) {
@@ -460,5 +494,6 @@ function buildTuiState(state, config, snapshot, positions, openOrders, strategyC
460
494
  openOrders,
461
495
  strategyState,
462
496
  tick: strategyCtx.tick,
497
+ exchangeBalances,
463
498
  };
464
499
  }
@@ -1,4 +1,4 @@
1
- export { loadBotConfig, quickGridConfig, quickDCAConfig, } from "./config.js";
1
+ export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "./config.js";
2
2
  export type { ConditionType, Condition, RiskConfig, GridStrategyParams, DCAStrategyParams, FundingArbStrategyParams, GenericStrategyParams, StrategyParams, BotConfig, } from "./config.js";
3
3
  export { runBot } from "./engine.js";
4
4
  export type { BotLog, BotOutputMode } from "./engine.js";
package/dist/bot/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // bot/ barrel — public API
2
- export { loadBotConfig, quickGridConfig, quickDCAConfig, } from "./config.js";
2
+ export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "./config.js";
3
3
  export { runBot } from "./engine.js";
4
4
  export { PRESETS, getPreset, getPresetsByStrategy } from "./presets.js";
5
5
  export { evaluateCondition, evaluateAllConditions, getMarketSnapshot, calculateRSI } from "./conditions.js";
@@ -1,3 +1,8 @@
1
+ export interface ExchangeBalance {
2
+ exchange: string;
3
+ equity: string;
4
+ available: string;
5
+ }
1
6
  export interface BotTuiState {
2
7
  phase: string;
3
8
  equity: number;
@@ -18,6 +23,7 @@ export interface BotTuiState {
18
23
  openOrders: OpenOrder[];
19
24
  strategyState: Record<string, unknown>;
20
25
  tick: number;
26
+ exchangeBalances?: ExchangeBalance[];
21
27
  }
22
28
  export interface Position {
23
29
  symbol: string;
@@ -26,6 +32,7 @@ export interface Position {
26
32
  entryPrice: string;
27
33
  markPrice: string;
28
34
  unrealizedPnl: string;
35
+ exchange?: string;
29
36
  }
30
37
  export interface OpenOrder {
31
38
  orderId: string;
@@ -68,9 +68,10 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
68
68
  const drawdownPct = state.peakEquity > 0 ? (drawdown / state.peakEquity) * 100 : 0;
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
- 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) }) })] }), _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)] })] })] })] }), _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: [_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) => {
71
+ const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
72
+ 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) }) })] }), _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) => {
72
73
  const upnl = parseFloat(p.unrealizedPnl);
73
- return (_jsxs(Box, { flexDirection: "row", children: [_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));
74
+ 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));
74
75
  })] }))] }), _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"] }) })] }));
75
76
  }
76
77
  // ── Utility ──
@@ -1,5 +1,5 @@
1
1
  export { BotDashboard } from "./BotDashboard.js";
2
- export type { BotTuiState, Position, OpenOrder, LogEntry, StateListener, LogListener, DashboardProps } from "./BotDashboard.js";
2
+ export type { BotTuiState, ExchangeBalance, Position, OpenOrder, LogEntry, StateListener, LogListener, DashboardProps } from "./BotDashboard.js";
3
3
  import type { DashboardProps } from "./BotDashboard.js";
4
4
  export declare function startDashboard(props: DashboardProps): {
5
5
  unmount: () => void;
@@ -1144,7 +1144,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1144
1144
  if (opts.compare)
1145
1145
  return handleCompare(isJson, opts.compare);
1146
1146
  if (opts.rates)
1147
- return handleRates(isJson, { symbol: opts.symbol, symbols: opts.symbols, all: opts.all, minSpread: opts.min });
1147
+ return handleRates(isJson, { symbol: opts.symbol, symbols: opts.symbols, all: opts.all, minSpread: opts.min, top: opts.top });
1148
1148
  if (opts.basis)
1149
1149
  return handleBasisScan(isJson, { minBasis: opts.min, symbol: typeof opts.basis === "string" ? opts.basis : opts.symbol });
1150
1150
  if (opts.hip3)
@@ -1226,7 +1226,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1226
1226
  if (!isJson())
1227
1227
  console.log(chalk.cyan(" Scanning perp-perp funding rate spreads...\n"));
1228
1228
  const spreads = await fetchFundingSpreads();
1229
- const filtered = spreads.filter(s => Math.abs(s.spread) >= minSpread);
1229
+ let filtered = spreads.filter(s => Math.abs(s.spread) >= minSpread);
1230
+ if (opts.top)
1231
+ filtered = filtered.slice(0, parseInt(opts.top));
1230
1232
  if (isJson()) {
1231
1233
  const perpPerp = filtered.map(s => {
1232
1234
  const grossSpread = Math.abs(s.spread);
@@ -25,6 +25,7 @@ export declare function handleRates(isJson: () => boolean, opts: {
25
25
  symbols?: string;
26
26
  all?: boolean;
27
27
  minSpread: string;
28
+ top?: string;
28
29
  }): Promise<void>;
29
30
  export declare function handleBasisScan(isJson: () => boolean, opts: {
30
31
  minBasis: string;
@@ -278,6 +278,8 @@ export async function handleRates(isJson, opts) {
278
278
  saveFundingSnapshot(allRates);
279
279
  }
280
280
  catch { /* non-critical */ }
281
+ if (opts.top)
282
+ snapshot.symbols = snapshot.symbols.slice(0, parseInt(opts.top));
281
283
  if (isJson()) {
282
284
  // Enrich with 24h rate history for each symbol
283
285
  const now = new Date();
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk } from "../utils.js";
3
- import { loadBotConfig, quickGridConfig, quickDCAConfig, runBot, PRESETS, getPreset, getPresetsByStrategy } from "../bot/index.js";
3
+ import { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, runBot, PRESETS, getPreset, getPresetsByStrategy } from "../bot/index.js";
4
4
  import { updateJobState } from "../jobs.js";
5
5
  import { runTWAP, runFundingArb, runGrid, runDCA, runTrailingStop } from "../strategies/index.js";
6
6
  export function registerBotCommands(program, getAdapter, getAdapterFor, isJson) {
@@ -88,7 +88,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
88
88
  // ── bot quick dca ──
89
89
  bot
90
90
  .command("quick-dca <symbol> <side> <amount> <interval>")
91
- .description("Quick-start a DCA bot")
91
+ .description("Quick-start a DCA bot (interval: seconds between orders)")
92
92
  .option("--orders <n>", "Total orders (0 = unlimited)", "0")
93
93
  .option("--price-limit <price>", "Don't buy above / sell below this price")
94
94
  .option("--trigger-drop <pct>", "Only buy when price drops X% from recent high")
@@ -343,6 +343,8 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
343
343
  .option("--max-slots <n>", "Max concurrent positions", "3")
344
344
  .option("--daily-limit <usd>", "Daily loss limit in USD", "250")
345
345
  .option("--headless", "Run without TUI dashboard")
346
+ .option("--background", "Run in background (tmux)")
347
+ .option("--job-id <id>", "Job ID")
346
348
  .action(async (symbol, opts) => {
347
349
  const adapter = await getAdapter();
348
350
  const sym = (symbol ?? "ETH").toUpperCase();
@@ -366,9 +368,27 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
366
368
  },
367
369
  monitor_interval_sec: 10,
368
370
  };
371
+ if (opts.background) {
372
+ const { startJob } = await import("../jobs.js");
373
+ const cliArgs = [
374
+ `-e`, adapter.name, `apex`, sym,
375
+ `--preset`, opts.preset, `--max-slots`, opts.maxSlots,
376
+ `--daily-limit`, opts.dailyLimit,
377
+ ];
378
+ const job = startJob({
379
+ strategy: `bot:apex`,
380
+ exchange: adapter.name,
381
+ params: { symbol: sym, ...opts },
382
+ cliArgs,
383
+ });
384
+ if (isJson())
385
+ return printJson(jsonOk(job));
386
+ printBotJobStarted(config.name, job.id);
387
+ return;
388
+ }
369
389
  const mode = resolveOutputMode(isJson, opts.headless);
370
390
  const log = makeLog();
371
- await runBot(adapter, config, undefined, log, undefined, mode);
391
+ await runBot(adapter, config, opts.jobId, log, undefined, mode);
372
392
  });
373
393
  // ── bot reflect ──
374
394
  bot
@@ -411,12 +431,24 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
411
431
  // ── bot run <strategy> <symbol> ──
412
432
  bot
413
433
  .command("run <strategy> [symbol]")
414
- .description("Run any registered strategy (symbol optional for multi-symbol strategies like funding-auto)")
434
+ .description("Run a strategy (use 'perp bot list-strategies' to see all). Symbol optional for multi-symbol strategies.")
415
435
  .option("--config <path>", "YAML/JSON config file")
416
436
  .option("--headless", "Run without TUI dashboard")
417
437
  .option("--param <key=value>", "Strategy parameter (repeatable)", (val, acc) => [...acc, val], [])
418
438
  .action(async (strategyName, symbol, opts) => {
419
439
  const sym = symbol?.toUpperCase() || "ALL";
440
+ // Strategies that require an explicit symbol
441
+ const symbolRequiredStrategies = [
442
+ "grid", "dca", "simple-mm", "engine-mm", "avellaneda-mm",
443
+ "momentum-breakout", "mean-reversion", "aggressive-taker",
444
+ "grid-mm", "liquidation-mm", "regime-mm",
445
+ ];
446
+ if (sym === "ALL" && symbolRequiredStrategies.includes(strategyName)) {
447
+ console.error(chalk.red(`\n Error: The '${strategyName}' strategy requires a <symbol> argument.`));
448
+ console.error(chalk.gray(` Usage: perp bot run ${strategyName} <symbol>\n`));
449
+ process.exitCode = 1;
450
+ return;
451
+ }
420
452
  // Load or build config
421
453
  let config;
422
454
  if (opts.config) {
@@ -445,11 +477,12 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
445
477
  params[key] = raw;
446
478
  }
447
479
  const adapter = await getAdapter();
480
+ const strategy = parseStrategy(strategyName, params);
448
481
  config = {
449
482
  name: `${strategyName}-${sym.toLowerCase()}-${Date.now().toString(36)}`,
450
483
  exchange: adapter.name,
451
484
  symbol: sym,
452
- strategy: params,
485
+ strategy,
453
486
  entry_conditions: [{ type: "always", value: 0 }],
454
487
  exit_conditions: [],
455
488
  risk: {
@@ -464,10 +497,14 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
464
497
  }
465
498
  // Verify strategy is registered
466
499
  await import("../bot/engine.js");
467
- const { getStrategy } = await import("../bot/strategy-registry.js");
500
+ const { getStrategy, listStrategies: listStrats } = await import("../bot/strategy-registry.js");
468
501
  if (!getStrategy(strategyName)) {
502
+ const available = listStrats();
469
503
  console.error(chalk.red(`\n Unknown strategy: "${strategyName}"`));
470
- console.error(chalk.gray(` Run 'perp bot list-strategies' to see available strategies.\n`));
504
+ console.error(chalk.gray(`\n Available strategies:`));
505
+ for (const s of available)
506
+ console.error(chalk.gray(` - ${s}`));
507
+ console.error();
471
508
  return;
472
509
  }
473
510
  const adapter = await getAdapter();
@@ -3,7 +3,7 @@ import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
3
3
  import { logExecution } from "../execution-log.js";
4
4
  import { executeBestBridge, getAllQuotes, checkDebridgeStatus, checkBridgeGasBalance, CHAIN_IDS, USDC_ADDRESSES, EXCHANGE_TO_CHAIN, } from "../bridge-engine.js";
5
5
  export function registerBridgeCommands(program, isJson) {
6
- const bridge = program.command("bridge").description("Cross-chain USDC bridge (deBridge DLN)");
6
+ const bridge = program.command("bridge").description("Cross-chain USDC bridge (deBridge DLN). See also: perp funds bridge (CCTP V2)");
7
7
  // ── bridge chains ──
8
8
  bridge
9
9
  .command("chains")
@@ -628,7 +628,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
628
628
  // ═══════════════════════════════════════════════════════
629
629
  funds
630
630
  .command("bridge")
631
- .description("Bridge USDC between chains via CCTP V2")
631
+ .description("Bridge USDC between chains via CCTP V2. See also: perp bridge (deBridge DLN)")
632
632
  .requiredOption("--from <chain>", "Source chain (arbitrum, ethereum)")
633
633
  .requiredOption("--to <chain>", "Destination chain (arbitrum, ethereum, solana)")
634
634
  .requiredOption("--amount <amount>", "USDC amount")
@@ -75,7 +75,11 @@ export function registerHistoryCommands(program, isJson, getAdapterForExchange)
75
75
  : chalk.red;
76
76
  const sideColor = r.side === "buy" || r.side === "long" ? chalk.green : chalk.red;
77
77
  const time = new Date(r.timestamp).toLocaleString();
78
- const notional = r.notional ? `$${formatUsd(r.notional)}` : r.price ? `$${formatUsd(Number(r.price) * Number(r.size))}` : "-";
78
+ const metaPrice = r.meta?.price ?? r.meta?.markPrice ?? r.meta?.entryPrice;
79
+ const effectivePrice = r.price ?? (typeof metaPrice === "number" || typeof metaPrice === "string" ? String(metaPrice) : undefined);
80
+ const sizeNum = Number(r.size);
81
+ const priceNum = effectivePrice ? Number(effectivePrice) : 0;
82
+ const notional = r.notional ? `$${formatUsd(r.notional)}` : (priceNum > 0 && sizeNum > 0) ? `$${formatUsd(priceNum * sizeNum)}` : "-";
79
83
  return [
80
84
  chalk.gray(time),
81
85
  chalk.white(r.exchange),
@@ -163,7 +163,7 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
163
163
  if (isJson())
164
164
  return printJson(jsonError("SYMBOL_NOT_FOUND", `Market ${sym} not found`));
165
165
  console.log(chalk.red(`\n Market "${sym}" not found.\n`));
166
- return;
166
+ return process.exit(1);
167
167
  }
168
168
  if (isJson())
169
169
  return printJson(jsonOk(m));
@@ -228,9 +228,10 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
228
228
  market
229
229
  .command("trades <symbol>")
230
230
  .description("Recent trades for a symbol")
231
- .action(async (symbol) => {
231
+ .option("--limit <n>", "Number of recent trades to show", "20")
232
+ .action(async (symbol, opts) => {
232
233
  const adapter = await getAdapter();
233
- const trades = await adapter.getRecentTrades(symbol.toUpperCase(), 20);
234
+ const trades = await adapter.getRecentTrades(symbol.toUpperCase(), parseInt(opts.limit));
234
235
  if (isJson())
235
236
  return printJson(jsonOk(trades));
236
237
  if (trades.length === 0) {
@@ -258,12 +259,20 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
258
259
  console.log(chalk.gray("\n No funding history.\n"));
259
260
  return;
260
261
  }
261
- const rows = history.map((h) => [
262
- new Date(h.time).toLocaleString(),
263
- formatPercent(h.rate),
264
- h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"),
265
- ]);
266
- console.log(makeTable(["Time", "Funding Rate", "Oracle"], rows));
262
+ const hasOracle = history.some((h) => h.price != null && h.price !== "0" && h.price !== "");
263
+ const rows = history.map((h) => {
264
+ const base = [
265
+ new Date(h.time).toLocaleString(),
266
+ formatPercent(h.rate),
267
+ ];
268
+ if (hasOracle)
269
+ base.push(h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"));
270
+ return base;
271
+ });
272
+ const headers = hasOracle ? ["Time", "Funding Rate", "Oracle"] : ["Time", "Funding Rate"];
273
+ console.log(makeTable(headers, rows));
274
+ if (!hasOracle)
275
+ console.log(chalk.gray(" Note: Oracle price not available for this exchange.\n"));
267
276
  });
268
277
  market
269
278
  .command("mid <symbol>")
@@ -367,7 +376,7 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
367
376
  console.log(chalk.cyan.bold("\n HIP-3 Deployed Perp DEXes\n"));
368
377
  const rows = dexes.map(d => [
369
378
  chalk.white.bold(d.name),
370
- chalk.gray(d.deployer.slice(0, 10) + "..."),
379
+ chalk.gray(d.deployer ? d.deployer.slice(0, 6) + "..." + d.deployer.slice(-4) : "-"),
371
380
  String(d.assets.length),
372
381
  d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
373
382
  ]);
@@ -244,7 +244,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
244
244
  trade
245
245
  .command("limit <symbol> <side> <price> <size>")
246
246
  .description("Place a limit order")
247
- .option("--tif <tif>", "Time in force: GTC, IOC, ALO, TOB", "GTC")
247
+ .option("--tif <tif>", "Time in force: GTC, IOC, ALO (HL only), TOB (HL only)", "GTC")
248
248
  .option("--reduce-only", "Reduce only order")
249
249
  .option("--client-id <id>", "Client order ID for idempotent tracking")
250
250
  .option("--auto-id", "Auto-generate a client order ID")
@@ -494,7 +494,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
494
494
  // === TP/SL — Pacifica + Hyperliquid ===
495
495
  trade
496
496
  .command("tpsl <symbol> <side>")
497
- .description("Set take-profit / stop-loss on a position")
497
+ .description("Set take-profit / stop-loss on a position (side: the side to CLOSE — buy to close short, sell to close long)")
498
498
  .option("--tp <price>", "Take profit trigger price")
499
499
  .option("--tp-limit <price>", "Take profit limit price")
500
500
  .option("--sl <price>", "Stop loss trigger price")
@@ -558,7 +558,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
558
558
  trade
559
559
  .command("scale-tp <symbol>")
560
560
  .description("Place multiple take-profit limit orders at different price levels (분할익절)")
561
- .requiredOption("--levels <levels>", "Comma-separated price:percent pairs (e.g., 72000:25,75000:25,80000:50)")
561
+ .requiredOption("--levels <levels>", "[required] Comma-separated price:percent pairs (e.g., 72000:25,75000:25,80000:50)")
562
562
  .option("--size <size>", "Override total position size (default: uses current position)")
563
563
  .action(async (symbol, opts) => {
564
564
  const sym = symbol.toUpperCase();
@@ -14,6 +14,10 @@ export declare class AsterAdapter implements ExchangeAdapter {
14
14
  private _testnet;
15
15
  constructor(apiKey?: string, apiSecret?: string, testnet?: boolean);
16
16
  get isReadOnly(): boolean;
17
+ /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
18
+ private _toApi;
19
+ /** Aster API symbol → CLI symbol (ETHUSDT → ETH) */
20
+ private _fromApi;
17
21
  init(): Promise<void>;
18
22
  getMarkets(): Promise<ExchangeMarketInfo[]>;
19
23
  getOrderbook(symbol: string): Promise<{