perp-cli 0.7.2 → 0.7.4

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 (41) 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 +2 -1
  6. package/dist/bot/config.js +3 -1
  7. package/dist/bot/engine.js +23 -13
  8. package/dist/bot/presets.js +1 -0
  9. package/dist/bot/strategies/dca-strategy.js +8 -4
  10. package/dist/bot/strategies/funding-arb-strategy.d.ts +1 -1
  11. package/dist/bot/strategies/funding-arb-strategy.js +126 -14
  12. package/dist/bot/strategies/grid-strategy.js +56 -51
  13. package/dist/cli-spec.js +1 -1
  14. package/dist/commands/account.js +1 -1
  15. package/dist/commands/arb/index.js +12 -4
  16. package/dist/commands/arb-auto.d.ts +2 -0
  17. package/dist/commands/arb-auto.js +37 -16
  18. package/dist/commands/arb.js +25 -6
  19. package/dist/commands/bot.js +5 -6
  20. package/dist/commands/dashboard.js +1 -1
  21. package/dist/commands/history.js +1 -1
  22. package/dist/commands/init.d.ts +2 -2
  23. package/dist/commands/init.js +5 -0
  24. package/dist/commands/market.js +1 -1
  25. package/dist/commands/rebalance.js +3 -3
  26. package/dist/commands/risk.js +1 -1
  27. package/dist/commands/settings.js +1 -1
  28. package/dist/commands/wallet.js +2 -2
  29. package/dist/config.d.ts +1 -1
  30. package/dist/config.js +1 -0
  31. package/dist/constants.js +1 -0
  32. package/dist/exchanges/index.d.ts +1 -0
  33. package/dist/exchanges/index.js +1 -0
  34. package/dist/exchanges/registry.js +5 -0
  35. package/dist/funding/normalize.js +1 -0
  36. package/dist/funding/rates.d.ts +1 -1
  37. package/dist/funding/rates.js +36 -3
  38. package/dist/index.js +19 -6
  39. package/dist/strategies/funding-arb.js +28 -5
  40. package/dist/utils.js +6 -1
  41. package/package.json +1 -1
@@ -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);
@@ -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) {
@@ -7,5 +7,6 @@ export type { AdapterRegistration } from "./registry.js";
7
7
  export { PacificaAdapter } from "./pacifica.js";
8
8
  export { HyperliquidAdapter } from "./hyperliquid.js";
9
9
  export { LighterAdapter } from "./lighter.js";
10
+ export { AsterAdapter } from "./aster.js";
10
11
  export { HyperliquidSpotAdapter } from "./hyperliquid-spot.js";
11
12
  export { LighterSpotAdapter } from "./lighter-spot.js";
@@ -6,5 +6,6 @@ export { registerAdapter, getAdapterRegistration, listExchanges, resolveExchange
6
6
  export { PacificaAdapter } from "./pacifica.js";
7
7
  export { HyperliquidAdapter } from "./hyperliquid.js";
8
8
  export { LighterAdapter } from "./lighter.js";
9
+ export { AsterAdapter } from "./aster.js";
9
10
  export { HyperliquidSpotAdapter } from "./hyperliquid-spot.js";
10
11
  export { LighterSpotAdapter } from "./lighter-spot.js";
@@ -35,3 +35,8 @@ registerAdapter({
35
35
  aliases: ["lt"],
36
36
  chain: "ethereum",
37
37
  });
38
+ registerAdapter({
39
+ name: "aster",
40
+ aliases: ["ast"],
41
+ chain: "bnb",
42
+ });
@@ -18,6 +18,7 @@ const EXCHANGE_FUNDING_HOURS = {
18
18
  hyperliquid: 1,
19
19
  pacifica: 1,
20
20
  lighter: 8,
21
+ aster: 8,
21
22
  };
22
23
  /**
23
24
  * Get funding period in hours for an exchange.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { type HistoricalAverages } from "./history.js";
9
9
  export interface ExchangeFundingRate {
10
- exchange: "pacifica" | "hyperliquid" | "lighter";
10
+ exchange: "pacifica" | "hyperliquid" | "lighter" | "aster";
11
11
  symbol: string;
12
12
  fundingRate: number;
13
13
  hourlyRate: number;
@@ -81,6 +81,38 @@ async function fetchLighterRates() {
81
81
  return [];
82
82
  }
83
83
  }
84
+ async function fetchAsterRates() {
85
+ try {
86
+ const [premiums, tickers] = await Promise.all([
87
+ fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()),
88
+ fetch("https://fapi.asterdex.com/fapi/v1/ticker/24hr").then(r => r.json()),
89
+ ]);
90
+ const priceMap = new Map();
91
+ for (const t of tickers ?? []) {
92
+ priceMap.set(String(t.symbol), Number(t.lastPrice ?? 0));
93
+ }
94
+ return (premiums ?? [])
95
+ .filter(p => String(p.symbol ?? "").endsWith("USDT"))
96
+ .map(p => {
97
+ const rawSymbol = String(p.symbol ?? "");
98
+ const symbol = rawSymbol.replace(/USDT$/, "");
99
+ const rate = Number(p.lastFundingRate ?? 0);
100
+ const hourly = toHourlyRate(rate, "aster");
101
+ return {
102
+ exchange: "aster",
103
+ symbol,
104
+ fundingRate: rate,
105
+ hourlyRate: hourly,
106
+ annualizedPct: annualizeRate(rate, "aster"),
107
+ markPrice: Number(p.markPrice ?? priceMap.get(rawSymbol) ?? 0),
108
+ nextFundingTime: Number(p.nextFundingTime ?? 0),
109
+ };
110
+ });
111
+ }
112
+ catch {
113
+ return [];
114
+ }
115
+ }
84
116
  // ── Core comparison logic ──
85
117
  /**
86
118
  * Fetch funding rates from all 3 DEXs in parallel, normalize, and compare.
@@ -88,12 +120,13 @@ async function fetchLighterRates() {
88
120
  */
89
121
  export async function fetchAllFundingRates(opts) {
90
122
  const exchangeStatus = {};
91
- const [pacRates, hlRates, ltRates] = await Promise.all([
123
+ const [pacRates, hlRates, ltRates, astRates] = await Promise.all([
92
124
  fetchPacificaRates().then(r => { exchangeStatus.pacifica = r.length > 0 ? "ok" : "error"; return r; }),
93
125
  fetchHyperliquidRates().then(r => { exchangeStatus.hyperliquid = r.length > 0 ? "ok" : "error"; return r; }),
94
126
  fetchLighterRates().then(r => { exchangeStatus.lighter = r.length > 0 ? "ok" : "error"; return r; }),
127
+ fetchAsterRates().then(r => { exchangeStatus.aster = r.length > 0 ? "ok" : "error"; return r; }),
95
128
  ]);
96
- const allRates = [...pacRates, ...hlRates, ...ltRates];
129
+ const allRates = [...pacRates, ...hlRates, ...ltRates, ...astRates];
97
130
  // Build per-symbol rate map
98
131
  const rateMap = new Map();
99
132
  for (const r of allRates) {
@@ -109,7 +142,7 @@ export async function fetchAllFundingRates(opts) {
109
142
  // Attach historical averages when available
110
143
  try {
111
144
  const symbols = Array.from(rateMap.keys());
112
- const exchanges = ["pacifica", "hyperliquid", "lighter"];
145
+ const exchanges = ["pacifica", "hyperliquid", "lighter", "aster"];
113
146
  const historicals = getHistoricalAverages(symbols, exchanges);
114
147
  for (const [, rates] of rateMap) {
115
148
  for (const r of rates) {
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ program
50
50
  .name("perp")
51
51
  .description("Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter)")
52
52
  .version(_pkg.version)
53
- .option("-e, --exchange <exchange>", `Exchange: pacifica, hyperliquid, lighter (default: ${_defaultExchange})`, _defaultExchange)
53
+ .option("-e, --exchange <exchange>", `Exchange: pacifica, hyperliquid, lighter, aster (default: ${_defaultExchange})`, _defaultExchange)
54
54
  .option("-n, --network <network>", "Network: mainnet or testnet", "mainnet")
55
55
  .option("-k, --private-key <key>", "Private key")
56
56
  .option("--json", "Output raw JSON (for piping)")
@@ -156,6 +156,13 @@ async function getAdapter() {
156
156
  _adapter = _lighterAdapter;
157
157
  break;
158
158
  }
159
+ case "aster": {
160
+ const { AsterAdapter } = await import("./exchanges/aster.js");
161
+ const ast = new AsterAdapter(undefined, undefined, isTestnet);
162
+ await ast.init();
163
+ _adapter = ast;
164
+ break;
165
+ }
159
166
  default:
160
167
  throw new Error(`Unknown exchange: ${exchange}`);
161
168
  }
@@ -271,6 +278,12 @@ async function getAdapterForExchange(rawExchange) {
271
278
  _adapter = _lighterAdapter;
272
279
  return _lighterAdapter;
273
280
  }
281
+ case "aster": {
282
+ const { AsterAdapter } = await import("./exchanges/aster.js");
283
+ const ast = new AsterAdapter(undefined, undefined, isTestnet);
284
+ await ast.init();
285
+ return ast;
286
+ }
274
287
  default:
275
288
  throw new Error(`Unknown exchange: ${exchange}`);
276
289
  }
@@ -334,8 +347,8 @@ program.command("status")
334
347
  const { fetchAllFundingRates, TOP_SYMBOLS } = await import("./funding-rates.js");
335
348
  const { saveFundingSnapshot, getHistoricalRates } = await import("./funding-history.js");
336
349
  await withJsonErrors(json, async () => {
337
- const EX_LIST = ["pacifica", "hyperliquid", "lighter"];
338
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
350
+ const EX_LIST = ["pacifica", "hyperliquid", "lighter", "aster"];
351
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : "AST";
339
352
  const snapshotPromises = EX_LIST.map(async (ex) => {
340
353
  try {
341
354
  const adapter = await getAdapterForExchange(ex);
@@ -606,7 +619,7 @@ program.hook("preAction", () => {
606
619
  });
607
620
  // Smart landing page: `perp` with no subcommand
608
621
  const rawArgs = process.argv.slice(2);
609
- const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && !["pacifica", "hyperliquid", "lighter", "hl", "lt", "pac", "mainnet", "testnet"].includes(a));
622
+ const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && !["pacifica", "hyperliquid", "lighter", "aster", "hl", "lt", "pac", "ast", "mainnet", "testnet"].includes(a));
610
623
  if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawArgs.includes("--help") && !rawArgs.includes("-V") && !rawArgs.includes("--version"))) {
611
624
  // No subcommand — show smart landing instead of help dump
612
625
  (async () => {
@@ -638,8 +651,8 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
638
651
  }
639
652
  else {
640
653
  // Configured — show exchange status + balance
641
- const EX_NAMES = ["pacifica", "hyperliquid", "lighter"];
642
- const exLabel = (e) => e === "pacifica" ? "Pacifica" : e === "hyperliquid" ? "Hyperliquid" : "Lighter";
654
+ const EX_NAMES = ["pacifica", "hyperliquid", "lighter", "aster"];
655
+ const exLabel = (e) => e === "pacifica" ? "Pacifica" : e === "hyperliquid" ? "Hyperliquid" : e === "lighter" ? "Lighter" : "Aster";
643
656
  // Ping + balance in parallel (with 5s timeout to keep landing fast)
644
657
  const withTimeout = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms))]);
645
658
  const statusResults = await Promise.allSettled(EX_NAMES.map(async (ex) => {
@@ -184,7 +184,7 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
184
184
  if (!liq.viable)
185
185
  continue;
186
186
  // Compute matched size (same for both legs)
187
- const matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx);
187
+ let matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx);
188
188
  if (!matched) {
189
189
  log(`[ARB] Skip ${symbol}: can't compute matched size (min notional or precision issue)`);
190
190
  continue;
@@ -199,10 +199,33 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
199
199
  }
200
200
  try {
201
201
  log(`[ARB] Opening: ${matched.size} ${symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
202
- await Promise.all([
203
- longAdapter.marketOrder(symbol, "buy", matched.size),
204
- shortAdapter.marketOrder(symbol, "sell", matched.size),
205
- ]);
202
+ try {
203
+ await Promise.all([
204
+ longAdapter.marketOrder(symbol, "buy", matched.size),
205
+ shortAdapter.marketOrder(symbol, "sell", matched.size),
206
+ ]);
207
+ }
208
+ catch (orderErr) {
209
+ const errMsg = orderErr instanceof Error ? orderErr.message : String(orderErr);
210
+ const lotMatch = errMsg.match(/not a multiple of lot size (\d+(?:\.\d+)?)/);
211
+ if (lotMatch) {
212
+ const lotSize = parseFloat(lotMatch[1]);
213
+ log(`[ARB] Lot size ${lotSize} detected, recalculating...`);
214
+ matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx, { lotSize });
215
+ if (!matched) {
216
+ log(`[ARB] Skip ${symbol}: can't meet lot size ${lotSize}`);
217
+ continue;
218
+ }
219
+ log(`[ARB] Retry: ${matched.size} ${symbol} ($${matched.notional.toFixed(0)}/leg)`);
220
+ await Promise.all([
221
+ longAdapter.marketOrder(symbol, "buy", matched.size),
222
+ shortAdapter.marketOrder(symbol, "sell", matched.size),
223
+ ]);
224
+ }
225
+ else {
226
+ throw orderErr;
227
+ }
228
+ }
206
229
  // Verify fills match, correct if needed
207
230
  try {
208
231
  const recon = await reconcileArbFills(longAdapter, shortAdapter, symbol, log);
package/dist/utils.js CHANGED
@@ -10,9 +10,14 @@ export function formatUsd(value) {
10
10
  const num = Number(value);
11
11
  if (isNaN(num))
12
12
  return String(value);
13
+ // Use more decimals for sub-cent values so they don't show as $0.00
14
+ const absNum = Math.abs(num);
15
+ const maxDecimals = absNum > 0 && absNum < 0.01 ? 6
16
+ : absNum < 1 ? 4
17
+ : 2;
13
18
  return num.toLocaleString("en-US", {
14
19
  minimumFractionDigits: 2,
15
- maximumFractionDigits: 2,
20
+ maximumFractionDigits: maxDecimals,
16
21
  });
17
22
  }
18
23
  export function formatPnl(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",