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.
- package/dist/arb/sizing.d.ts +1 -0
- package/dist/arb/sizing.js +25 -6
- package/dist/arb/utils.js +1 -0
- package/dist/bot/conditions.js +1 -2
- package/dist/bot/config.d.ts +2 -1
- package/dist/bot/config.js +3 -1
- package/dist/bot/engine.js +23 -13
- package/dist/bot/presets.js +1 -0
- package/dist/bot/strategies/dca-strategy.js +8 -4
- package/dist/bot/strategies/funding-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +126 -14
- package/dist/bot/strategies/grid-strategy.js +56 -51
- package/dist/cli-spec.js +1 -1
- package/dist/commands/account.js +1 -1
- package/dist/commands/arb/index.js +12 -4
- package/dist/commands/arb-auto.d.ts +2 -0
- package/dist/commands/arb-auto.js +37 -16
- package/dist/commands/arb.js +25 -6
- package/dist/commands/bot.js +5 -6
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/history.js +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +5 -0
- package/dist/commands/market.js +1 -1
- package/dist/commands/rebalance.js +3 -3
- package/dist/commands/risk.js +1 -1
- package/dist/commands/settings.js +1 -1
- package/dist/commands/wallet.js +2 -2
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -0
- package/dist/constants.js +1 -0
- package/dist/exchanges/index.d.ts +1 -0
- package/dist/exchanges/index.js +1 -0
- package/dist/exchanges/registry.js +5 -0
- package/dist/funding/normalize.js +1 -0
- package/dist/funding/rates.d.ts +1 -1
- package/dist/funding/rates.js +36 -3
- package/dist/index.js +19 -6
- package/dist/strategies/funding-arb.js +28 -5
- package/dist/utils.js +6 -1
- 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")
|
package/dist/commands/risk.js
CHANGED
|
@@ -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);
|
package/dist/commands/wallet.js
CHANGED
|
@@ -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,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";
|
package/dist/exchanges/index.js
CHANGED
|
@@ -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";
|
package/dist/funding/rates.d.ts
CHANGED
|
@@ -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;
|
package/dist/funding/rates.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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:
|
|
20
|
+
maximumFractionDigits: maxDecimals,
|
|
16
21
|
});
|
|
17
22
|
}
|
|
18
23
|
export function formatPnl(value) {
|