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
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",