perp-cli 0.7.1 → 0.7.3

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.
Files changed (53) hide show
  1. package/dist/arb/sizing.d.ts +1 -0
  2. package/dist/arb/sizing.js +25 -6
  3. package/dist/arb/utils.js +1 -0
  4. package/dist/bot/conditions.js +1 -2
  5. package/dist/bot/config.d.ts +3 -1
  6. package/dist/bot/config.js +4 -2
  7. package/dist/bot/engine.js +63 -18
  8. package/dist/bot/index.d.ts +1 -1
  9. package/dist/bot/index.js +1 -1
  10. package/dist/bot/presets.js +1 -0
  11. package/dist/bot/strategies/dca-strategy.js +8 -4
  12. package/dist/bot/strategies/funding-arb-strategy.d.ts +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +126 -14
  14. package/dist/bot/strategies/grid-strategy.js +56 -51
  15. package/dist/bot/tui/BotDashboard.d.ts +7 -0
  16. package/dist/bot/tui/BotDashboard.js +3 -2
  17. package/dist/bot/tui/index.d.ts +1 -1
  18. package/dist/cli-spec.js +1 -1
  19. package/dist/commands/account.js +1 -1
  20. package/dist/commands/arb/index.js +12 -4
  21. package/dist/commands/arb-auto.d.ts +2 -0
  22. package/dist/commands/arb-auto.js +41 -18
  23. package/dist/commands/arb.d.ts +1 -0
  24. package/dist/commands/arb.js +26 -5
  25. package/dist/commands/bot.js +47 -10
  26. package/dist/commands/bridge.js +1 -1
  27. package/dist/commands/dashboard.js +1 -1
  28. package/dist/commands/funds.js +1 -1
  29. package/dist/commands/history.js +6 -2
  30. package/dist/commands/init.d.ts +2 -2
  31. package/dist/commands/init.js +5 -0
  32. package/dist/commands/market.js +20 -11
  33. package/dist/commands/rebalance.js +3 -3
  34. package/dist/commands/risk.js +1 -1
  35. package/dist/commands/settings.js +1 -1
  36. package/dist/commands/trade.js +3 -3
  37. package/dist/commands/wallet.js +2 -2
  38. package/dist/config.d.ts +1 -1
  39. package/dist/config.js +1 -0
  40. package/dist/constants.js +1 -0
  41. package/dist/exchanges/aster.d.ts +4 -0
  42. package/dist/exchanges/aster.js +39 -28
  43. package/dist/exchanges/edgex.d.ts +62 -0
  44. package/dist/exchanges/edgex.js +552 -0
  45. package/dist/exchanges/index.d.ts +1 -0
  46. package/dist/exchanges/index.js +1 -0
  47. package/dist/exchanges/registry.js +5 -0
  48. package/dist/funding/normalize.js +1 -0
  49. package/dist/funding/rates.d.ts +1 -1
  50. package/dist/funding/rates.js +36 -3
  51. package/dist/index.js +19 -6
  52. package/dist/strategies/funding-arb.js +28 -5
  53. package/package.json +1 -1
@@ -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")
@@ -140,7 +140,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
140
140
  .option("--close-spread <pct>", "Close when spread drops below (%)", "5")
141
141
  .option("--size <usd>", "Position size per leg ($)", "50")
142
142
  .option("--max-positions <n>", "Max simultaneous positions", "3")
143
- .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter")
143
+ .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter,aster")
144
144
  .option("--interval <sec>", "Check interval in seconds", "60")
145
145
  .option("--max-drawdown <usd>", "Stop if drawdown exceeds ($)", "200")
146
146
  .option("--background", "Run in background (tmux)")
@@ -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();
@@ -479,7 +516,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
479
516
  const multiExchangeStrategies = ["funding-auto", "funding-arb", "funding-arb-v2", "basis-arb", "hedge-agent"];
480
517
  if (multiExchangeStrategies.includes(strategyName) && getAdapterFor) {
481
518
  extraAdapters = new Map();
482
- for (const ex of ["pacifica", "hyperliquid", "lighter"]) {
519
+ for (const ex of ["pacifica", "hyperliquid", "lighter", "aster"]) {
483
520
  if (ex === adapter.name)
484
521
  continue;
485
522
  try {
@@ -647,7 +684,7 @@ function registerRunSubcommands(parent, getAdapter, getAdapterFor, isJson) {
647
684
  .option("--interval <sec>", "Check interval in seconds", "60")
648
685
  .option("--auto-execute", "Actually place trades (default: monitor only)")
649
686
  .option("--max-positions <n>", "Max simultaneous positions", "3")
650
- .option("--exchanges <list>", "Comma-separated exchanges to use", "pacifica,hyperliquid,lighter")
687
+ .option("--exchanges <list>", "Comma-separated exchanges to use", "pacifica,hyperliquid,lighter,aster")
651
688
  .option("--auto-rebalance", "Trigger rebalance alerts when exchange balance is low")
652
689
  .option("--rebalance-threshold <usd>", "Rebalance when available < this ($)", "100")
653
690
  .option("--max-drawdown <usd>", "Close all positions if total uPnL exceeds this loss ($)")
@@ -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")
@@ -50,7 +50,7 @@ export function registerDashboardCommands(program, getAdapterForExchange, isJson
50
50
  .description("Launch live web dashboard for real-time portfolio monitoring")
51
51
  .option("-p, --port <port>", "Port to serve dashboard on (auto-finds free port)", "3456")
52
52
  .option("--interval <ms>", "Poll interval in milliseconds", "5000")
53
- .option("--exchanges <list>", "Comma-separated exchanges to monitor", "pacifica,hyperliquid,lighter")
53
+ .option("--exchanges <list>", "Comma-separated exchanges to monitor", "pacifica,hyperliquid,lighter,aster")
54
54
  .option("--dex <list>", "Comma-separated HIP-3 dexes to monitor (e.g. xyz,flx,hyna)")
55
55
  .option("--no-auto-dex", "Disable auto-detection of HIP-3 dexes with active positions")
56
56
  .action(async (opts) => {
@@ -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")
@@ -4,7 +4,7 @@ import { readExecutionLog, getExecutionStats, pruneExecutionLog } from "../execu
4
4
  import { loadArbState } from "../arb/index.js";
5
5
  import { readPositionHistory, getPositionStats } from "../position-history.js";
6
6
  import { saveEquitySnapshot, readEquityHistory, computePnlMetrics, computeDailyPnl, aggregateWeekly, } from "../equity-tracker.js";
7
- const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
7
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
8
8
  function parseSince(since) {
9
9
  if (!since)
10
10
  return undefined;
@@ -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),
@@ -5,10 +5,10 @@ declare function loadEnvFile(): Record<string, string>;
5
5
  declare function setEnvVar(key: string, value: string): void;
6
6
  export declare const EXCHANGE_ENV_MAP: Record<string, {
7
7
  envKey: string;
8
- chain: "solana" | "evm";
8
+ chain: "solana" | "evm" | "apikey";
9
9
  label: string;
10
10
  }>;
11
- export declare function validateKey(chain: "solana" | "evm", key: string): Promise<{
11
+ export declare function validateKey(chain: "solana" | "evm" | "apikey", key: string): Promise<{
12
12
  valid: boolean;
13
13
  address: string;
14
14
  }>;
@@ -70,6 +70,7 @@ export const EXCHANGE_ENV_MAP = {
70
70
  pacifica: { envKey: "PACIFICA_PRIVATE_KEY", chain: "solana", label: "Pacifica (Solana)" },
71
71
  hyperliquid: { envKey: "HL_PRIVATE_KEY", chain: "evm", label: "Hyperliquid (EVM)" },
72
72
  lighter: { envKey: "LIGHTER_PRIVATE_KEY", chain: "evm", label: "Lighter (EVM)" },
73
+ aster: { envKey: "ASTER_API_KEY", chain: "apikey", label: "Aster (BNB Chain)" },
73
74
  };
74
75
  // ── Validate keys ─────────────────────────────────────────────
75
76
  export async function validateKey(chain, key) {
@@ -91,6 +92,10 @@ export async function validateKey(chain, key) {
91
92
  return { valid: false, address: "" };
92
93
  }
93
94
  }
95
+ else if (chain === "apikey") {
96
+ // API key validation: just check it's a non-empty hex-like string
97
+ return { valid: key.length >= 16, address: key.slice(0, 8) + "..." + key.slice(-4) };
98
+ }
94
99
  else {
95
100
  try {
96
101
  const { ethers } = await import("ethers");
@@ -1,7 +1,7 @@
1
1
  import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
2
2
  import chalk from "chalk";
3
3
  import { hasPacificaSdk, isDexCapable } from "../exchanges/capabilities.js";
4
- const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
4
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
5
5
  export function registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange) {
6
6
  const market = program.command("market").description("Market data commands");
7
7
  market
@@ -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
  ]);
@@ -9,7 +9,7 @@ export function registerRebalanceCommands(program, getAdapterForExchange, isJson
9
9
  rebalance
10
10
  .command("check")
11
11
  .description("Show balances across all exchanges")
12
- .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter")
12
+ .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter,aster")
13
13
  .action(async (opts) => {
14
14
  const exchangeNames = opts.exchanges.split(",").map((e) => e.trim());
15
15
  const adapters = new Map();
@@ -55,7 +55,7 @@ export function registerRebalanceCommands(program, getAdapterForExchange, isJson
55
55
  rebalance
56
56
  .command("plan")
57
57
  .description("Calculate optimal rebalancing moves")
58
- .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter")
58
+ .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter,aster")
59
59
  .option("--min-move <usd>", "Minimum move amount ($)", "50")
60
60
  .option("--reserve <usd>", "Keep at least this much per exchange ($)", "20")
61
61
  .action(async (opts) => {
@@ -115,7 +115,7 @@ export function registerRebalanceCommands(program, getAdapterForExchange, isJson
115
115
  rebalance
116
116
  .command("execute")
117
117
  .description("Execute rebalancing (withdraw → bridge → deposit)")
118
- .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter")
118
+ .option("--exchanges <list>", "Comma-separated exchanges", "pacifica,hyperliquid,lighter,aster")
119
119
  .option("--min-move <usd>", "Minimum move amount ($)", "50")
120
120
  .option("--reserve <usd>", "Keep at least this much per exchange ($)", "20")
121
121
  .option("--dry-run", "Show what would happen without executing")
@@ -47,7 +47,7 @@ export async function runHealthCheck(isJson) {
47
47
  const overall = allOk ? chalk.green("ALL HEALTHY") : chalk.red("ISSUES DETECTED");
48
48
  console.log(`\n Overall: ${overall}\n`);
49
49
  }
50
- const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
50
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
51
51
  export function registerRiskCommands(program, getAdapterForExchange, isJson) {
52
52
  const risk = program.command("risk").description("Risk management and guardrails");
53
53
  // ── risk status ──
@@ -15,7 +15,7 @@ function resolveEnvKey(nameOrKey) {
15
15
  if (info)
16
16
  return { envKey: info.envKey, chain: info.chain };
17
17
  // Common aliases
18
- const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
18
+ const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter", ast: "aster" };
19
19
  const aliased = aliases[nameOrKey.toLowerCase()];
20
20
  if (aliased)
21
21
  return resolveEnvKey(aliased);
@@ -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();
@@ -363,10 +363,10 @@ export function registerWalletCommands(program, isJson) {
363
363
  if (!info) {
364
364
  if (isJson()) {
365
365
  const { jsonError } = await import("../utils.js");
366
- return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter (or hl, pac, lt)`));
366
+ return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter, aster (or hl, pac, lt, ast)`));
367
367
  }
368
368
  console.error(chalk.red(`\n Unknown exchange: ${exchange}`));
369
- console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter (or hl, pac, lt)\n`));
369
+ console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter, aster (or hl, pac, lt, ast)\n`));
370
370
  process.exit(1);
371
371
  }
372
372
  const { valid, address } = await validateKey(info.chain, key);
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Keypair } from "@solana/web3.js";
2
- export type Exchange = "pacifica" | "hyperliquid" | "lighter";
2
+ export type Exchange = "pacifica" | "hyperliquid" | "lighter" | "aster";
3
3
  /**
4
4
  * Try to load a private key without throwing if none is found.
5
5
  * Returns null if no key is configured.
package/dist/config.js CHANGED
@@ -32,6 +32,7 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
32
32
  pacifica: ["PACIFICA_PRIVATE_KEY", "pk"],
33
33
  hyperliquid: ["HYPERLIQUID_PRIVATE_KEY", "HL_PRIVATE_KEY"],
34
34
  lighter: ["LIGHTER_PRIVATE_KEY"],
35
+ aster: ["ASTER_API_KEY"],
35
36
  };
36
37
  for (const envVar of envMap[exchange]) {
37
38
  if (process.env[envVar])
package/dist/constants.js CHANGED
@@ -7,6 +7,7 @@ export const TAKER_FEES = {
7
7
  hyperliquid: 0.00035,
8
8
  pacifica: 0.00035,
9
9
  lighter: 0,
10
+ aster: 0.0004, // 0.04% taker fee
10
11
  };
11
12
  /** Get taker fee for an exchange. Uses settings if available, falls back to defaults. */
12
13
  export function getTakerFee(exchange) {
@@ -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<{
@@ -23,6 +23,17 @@ export class AsterAdapter {
23
23
  get isReadOnly() {
24
24
  return !this._apiKey || !this._apiSecret;
25
25
  }
26
+ /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
27
+ _toApi(symbol) {
28
+ const s = symbol.toUpperCase().replace(/-PERP$/, "");
29
+ if (s.endsWith("USDT") || s.endsWith("BUSD"))
30
+ return s;
31
+ return `${s}USDT`;
32
+ }
33
+ /** Aster API symbol → CLI symbol (ETHUSDT → ETH) */
34
+ _fromApi(symbol) {
35
+ return symbol.replace(/USDT$/, "").replace(/BUSD$/, "");
36
+ }
26
37
  async init() {
27
38
  // Verify connectivity by fetching server time
28
39
  await this._publicGet("/fapi/v1/time");
@@ -70,7 +81,7 @@ export class AsterAdapter {
70
81
  // Extract max leverage from leverage brackets if available
71
82
  const maxLev = Number(s.maxLeverage ?? 50);
72
83
  return {
73
- symbol: sym,
84
+ symbol: this._fromApi(sym),
74
85
  markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
75
86
  indexPrice: String(premium?.indexPrice ?? "0"),
76
87
  fundingRate: String(premium?.lastFundingRate ?? "0"),
@@ -81,7 +92,7 @@ export class AsterAdapter {
81
92
  });
82
93
  }
83
94
  async getOrderbook(symbol) {
84
- const res = await this._publicGet("/fapi/v1/depth", { symbol: symbol.toUpperCase(), limit: "50" });
95
+ const res = await this._publicGet("/fapi/v1/depth", { symbol: this._toApi(symbol), limit: "50" });
85
96
  return {
86
97
  bids: res?.bids ?? [],
87
98
  asks: res?.asks ?? [],
@@ -89,12 +100,12 @@ export class AsterAdapter {
89
100
  }
90
101
  async getRecentTrades(symbol, limit = 20) {
91
102
  const trades = await this._publicGet("/fapi/v1/trades", {
92
- symbol: symbol.toUpperCase(),
103
+ symbol: this._toApi(symbol),
93
104
  limit: String(limit),
94
105
  });
95
106
  return (trades ?? []).map((t) => ({
96
107
  time: Number(t.time ?? 0),
97
- symbol: symbol.toUpperCase(),
108
+ symbol: this._fromApi(String(t.symbol ?? symbol)),
98
109
  side: t.isBuyerMaker ? "sell" : "buy",
99
110
  price: String(t.price ?? "0"),
100
111
  size: String(t.qty ?? "0"),
@@ -103,7 +114,7 @@ export class AsterAdapter {
103
114
  }
104
115
  async getFundingHistory(symbol, limit = 10) {
105
116
  const data = await this._publicGet("/fapi/v1/fundingRate", {
106
- symbol: symbol.toUpperCase(),
117
+ symbol: this._toApi(symbol),
107
118
  limit: String(limit),
108
119
  });
109
120
  return (data ?? []).map((d) => ({
@@ -114,7 +125,7 @@ export class AsterAdapter {
114
125
  }
115
126
  async getKlines(symbol, interval, startTime, endTime) {
116
127
  const data = await this._publicGet("/fapi/v1/klines", {
117
- symbol: symbol.toUpperCase(),
128
+ symbol: this._toApi(symbol),
118
129
  interval,
119
130
  startTime: String(startTime),
120
131
  endTime: String(endTime),
@@ -151,7 +162,7 @@ export class AsterAdapter {
151
162
  .map((p) => {
152
163
  const amt = Number(p.positionAmt ?? 0);
153
164
  return {
154
- symbol: String(p.symbol ?? ""),
165
+ symbol: this._fromApi(String(p.symbol ?? "")),
155
166
  side: amt > 0 ? "long" : "short",
156
167
  size: String(Math.abs(amt)),
157
168
  entryPrice: String(p.entryPrice ?? "0"),
@@ -166,7 +177,7 @@ export class AsterAdapter {
166
177
  const orders = await this._signedGet("/fapi/v1/openOrders");
167
178
  return (orders ?? []).map((o) => ({
168
179
  orderId: String(o.orderId ?? ""),
169
- symbol: String(o.symbol ?? ""),
180
+ symbol: this._fromApi(String(o.symbol ?? "")),
170
181
  side: String(o.side).toLowerCase(),
171
182
  price: String(o.price ?? "0"),
172
183
  size: String(o.origQty ?? "0"),
@@ -178,15 +189,15 @@ export class AsterAdapter {
178
189
  async getOrderHistory(limit = 30) {
179
190
  // allOrders requires symbol — get from positions or recent trades
180
191
  const positions = await this.getPositions();
181
- const symbols = new Set(positions.map(p => p.symbol));
182
- if (symbols.size === 0) {
192
+ const apiSymbols = new Set(positions.map(p => this._toApi(p.symbol)));
193
+ if (apiSymbols.size === 0) {
183
194
  // Fallback: query the most common perp symbols when no open positions exist
184
195
  for (const s of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT"]) {
185
- symbols.add(s);
196
+ apiSymbols.add(s);
186
197
  }
187
198
  }
188
199
  const allOrders = [];
189
- for (const sym of symbols) {
200
+ for (const sym of apiSymbols) {
190
201
  try {
191
202
  const orders = await this._signedGet("/fapi/v1/allOrders", {
192
203
  symbol: sym,
@@ -195,7 +206,7 @@ export class AsterAdapter {
195
206
  for (const o of orders ?? []) {
196
207
  allOrders.push({
197
208
  orderId: String(o.orderId ?? ""),
198
- symbol: String(o.symbol ?? ""),
209
+ symbol: this._fromApi(String(o.symbol ?? "")),
199
210
  side: String(o.side).toLowerCase(),
200
211
  price: String(o.price ?? "0"),
201
212
  size: String(o.origQty ?? "0"),
@@ -211,11 +222,11 @@ export class AsterAdapter {
211
222
  }
212
223
  async getTradeHistory(limit = 30) {
213
224
  const positions = await this.getPositions();
214
- const symbols = new Set(positions.map(p => p.symbol));
215
- if (symbols.size === 0)
216
- symbols.add("BTCUSDT");
225
+ const apiSymbols = new Set(positions.map(p => this._toApi(p.symbol)));
226
+ if (apiSymbols.size === 0)
227
+ apiSymbols.add("BTCUSDT");
217
228
  const allTrades = [];
218
- for (const sym of symbols) {
229
+ for (const sym of apiSymbols) {
219
230
  try {
220
231
  const trades = await this._signedGet("/fapi/v1/userTrades", {
221
232
  symbol: sym,
@@ -224,7 +235,7 @@ export class AsterAdapter {
224
235
  for (const t of trades ?? []) {
225
236
  allTrades.push({
226
237
  time: Number(t.time ?? 0),
227
- symbol: String(t.symbol ?? ""),
238
+ symbol: this._fromApi(String(t.symbol ?? "")),
228
239
  side: t.buyer ? "buy" : "sell",
229
240
  price: String(t.price ?? "0"),
230
241
  size: String(t.qty ?? "0"),
@@ -243,14 +254,14 @@ export class AsterAdapter {
243
254
  });
244
255
  return (data ?? []).map((f) => ({
245
256
  time: Number(f.time ?? 0),
246
- symbol: String(f.symbol ?? ""),
257
+ symbol: this._fromApi(String(f.symbol ?? "")),
247
258
  payment: String(f.income ?? "0"),
248
259
  }));
249
260
  }
250
261
  // ── Trading ──
251
262
  async marketOrder(symbol, side, size) {
252
263
  return this._signedPost("/fapi/v1/order", {
253
- symbol: symbol.toUpperCase(),
264
+ symbol: this._toApi(symbol),
254
265
  side: side.toUpperCase(),
255
266
  type: "MARKET",
256
267
  quantity: size,
@@ -258,7 +269,7 @@ export class AsterAdapter {
258
269
  }
259
270
  async limitOrder(symbol, side, price, size, opts) {
260
271
  const params = {
261
- symbol: symbol.toUpperCase(),
272
+ symbol: this._toApi(symbol),
262
273
  side: side.toUpperCase(),
263
274
  type: "LIMIT",
264
275
  price,
@@ -279,7 +290,7 @@ export class AsterAdapter {
279
290
  }
280
291
  async cancelOrder(symbol, orderId) {
281
292
  return this._signedDelete("/fapi/v1/order", {
282
- symbol: symbol.toUpperCase(),
293
+ symbol: this._toApi(symbol),
283
294
  orderId,
284
295
  });
285
296
  }
@@ -287,14 +298,14 @@ export class AsterAdapter {
287
298
  if (!symbol) {
288
299
  // Cancel all for all symbols with open orders
289
300
  const orders = await this.getOpenOrders();
290
- const symbols = new Set(orders.map(o => o.symbol));
301
+ const apiSymbols = new Set(orders.map(o => this._toApi(o.symbol)));
291
302
  const results = [];
292
- for (const sym of symbols) {
303
+ for (const sym of apiSymbols) {
293
304
  results.push(await this._signedDelete("/fapi/v1/allOpenOrders", { symbol: sym }));
294
305
  }
295
306
  return results;
296
307
  }
297
- return this._signedDelete("/fapi/v1/allOpenOrders", { symbol: symbol.toUpperCase() });
308
+ return this._signedDelete("/fapi/v1/allOpenOrders", { symbol: this._toApi(symbol) });
298
309
  }
299
310
  // ── Risk ──
300
311
  async setLeverage(symbol, leverage, marginMode) {
@@ -302,7 +313,7 @@ export class AsterAdapter {
302
313
  if (marginMode) {
303
314
  try {
304
315
  await this._signedPost("/fapi/v1/marginType", {
305
- symbol: symbol.toUpperCase(),
316
+ symbol: this._toApi(symbol),
306
317
  marginType: marginMode === "cross" ? "CROSSED" : "ISOLATED",
307
318
  });
308
319
  }
@@ -311,13 +322,13 @@ export class AsterAdapter {
311
322
  }
312
323
  }
313
324
  return this._signedPost("/fapi/v1/leverage", {
314
- symbol: symbol.toUpperCase(),
325
+ symbol: this._toApi(symbol),
315
326
  leverage: String(leverage),
316
327
  });
317
328
  }
318
329
  async stopOrder(symbol, side, size, triggerPrice, opts) {
319
330
  const params = {
320
- symbol: symbol.toUpperCase(),
331
+ symbol: this._toApi(symbol),
321
332
  side: side.toUpperCase(),
322
333
  quantity: size,
323
334
  stopPrice: triggerPrice,