perp-cli 0.7.11 → 0.7.12
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/exchanges/aster.d.ts +5 -0
- package/dist/exchanges/aster.js +69 -55
- package/package.json +1 -1
|
@@ -12,6 +12,9 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
12
12
|
private _apiSecret;
|
|
13
13
|
private _baseUrl;
|
|
14
14
|
private _testnet;
|
|
15
|
+
private _marketsCache;
|
|
16
|
+
private _marketsCacheTime;
|
|
17
|
+
private static readonly CACHE_TTL;
|
|
15
18
|
constructor(apiKey?: string, apiSecret?: string, testnet?: boolean);
|
|
16
19
|
get isReadOnly(): boolean;
|
|
17
20
|
/** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
|
|
@@ -55,6 +58,8 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
55
58
|
routeType?: number;
|
|
56
59
|
}): Promise<unknown>;
|
|
57
60
|
private _sign;
|
|
61
|
+
/** Handle 429 rate limit with retry */
|
|
62
|
+
private _handleResponse;
|
|
58
63
|
private _publicGet;
|
|
59
64
|
private _signedGet;
|
|
60
65
|
private _signedPost;
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -12,6 +12,9 @@ export class AsterAdapter {
|
|
|
12
12
|
_apiSecret;
|
|
13
13
|
_baseUrl;
|
|
14
14
|
_testnet;
|
|
15
|
+
_marketsCache = null;
|
|
16
|
+
_marketsCacheTime = 0;
|
|
17
|
+
static CACHE_TTL = 10_000; // 10 seconds
|
|
15
18
|
constructor(apiKey, apiSecret, testnet = false) {
|
|
16
19
|
this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
|
|
17
20
|
this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
|
|
@@ -40,6 +43,10 @@ export class AsterAdapter {
|
|
|
40
43
|
}
|
|
41
44
|
// ── Market Data ──
|
|
42
45
|
async getMarkets() {
|
|
46
|
+
// Return cached result if fresh
|
|
47
|
+
if (this._marketsCache && Date.now() - this._marketsCacheTime < AsterAdapter.CACHE_TTL) {
|
|
48
|
+
return this._marketsCache;
|
|
49
|
+
}
|
|
43
50
|
const [info, tickers] = await Promise.all([
|
|
44
51
|
this._publicGet("/fapi/v1/exchangeInfo"),
|
|
45
52
|
this._publicGet("/fapi/v1/ticker/24hr"),
|
|
@@ -57,28 +64,12 @@ export class AsterAdapter {
|
|
|
57
64
|
catch { /* non-critical */ }
|
|
58
65
|
const tradingSymbols = (info?.symbols ?? [])
|
|
59
66
|
.filter((s) => String(s.contractType) === "PERPETUAL" && String(s.status) === "TRADING");
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
const oiMap = new Map();
|
|
63
|
-
const OI_BATCH = 20;
|
|
64
|
-
for (let i = 0; i < tradingSymbols.length; i += OI_BATCH) {
|
|
65
|
-
const batch = tradingSymbols.slice(i, i + OI_BATCH);
|
|
66
|
-
const results = await Promise.allSettled(batch.map(async (s) => {
|
|
67
|
-
const sym = String(s.symbol);
|
|
68
|
-
const res = await this._publicGet("/fapi/v1/openInterest", { symbol: sym });
|
|
69
|
-
return { sym, oi: String(res?.openInterest ?? "0") };
|
|
70
|
-
}));
|
|
71
|
-
for (const r of results) {
|
|
72
|
-
if (r.status === "fulfilled")
|
|
73
|
-
oiMap.set(r.value.sym, r.value.oi);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return tradingSymbols
|
|
67
|
+
// OI is fetched lazily per symbol (see getMarketInfo) to avoid 300+ API calls
|
|
68
|
+
const result = tradingSymbols
|
|
77
69
|
.map((s) => {
|
|
78
70
|
const sym = String(s.symbol);
|
|
79
71
|
const ticker = tickerMap.get(sym);
|
|
80
72
|
const premium = premiumMap.get(sym);
|
|
81
|
-
// Extract max leverage from leverage brackets if available
|
|
82
73
|
const maxLev = Number(s.maxLeverage ?? 50);
|
|
83
74
|
return {
|
|
84
75
|
symbol: this._fromApi(sym),
|
|
@@ -86,10 +77,13 @@ export class AsterAdapter {
|
|
|
86
77
|
indexPrice: String(premium?.indexPrice ?? "0"),
|
|
87
78
|
fundingRate: String(premium?.lastFundingRate ?? "0"),
|
|
88
79
|
volume24h: String(ticker?.quoteVolume ?? ticker?.volume ?? "0"),
|
|
89
|
-
openInterest:
|
|
80
|
+
openInterest: "0",
|
|
90
81
|
maxLeverage: maxLev,
|
|
91
82
|
};
|
|
92
83
|
});
|
|
84
|
+
this._marketsCache = result;
|
|
85
|
+
this._marketsCacheTime = Date.now();
|
|
86
|
+
return result;
|
|
93
87
|
}
|
|
94
88
|
async getOrderbook(symbol) {
|
|
95
89
|
const res = await this._publicGet("/fapi/v1/depth", { symbol: this._toApi(symbol), limit: "50" });
|
|
@@ -359,62 +353,82 @@ export class AsterAdapter {
|
|
|
359
353
|
.digest("hex");
|
|
360
354
|
return `${queryString}&signature=${signature}`;
|
|
361
355
|
}
|
|
356
|
+
/** Handle 429 rate limit with retry */
|
|
357
|
+
async _handleResponse(res, method, path, attempt = 0) {
|
|
358
|
+
if (res.status === 429) {
|
|
359
|
+
if (attempt >= 2)
|
|
360
|
+
throw new Error(`${method} ${path} rate limited after ${attempt + 1} attempts`);
|
|
361
|
+
const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
|
|
362
|
+
const waitMs = Math.min(retryAfter * 1000, 30000);
|
|
363
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
364
|
+
return null; // signal retry
|
|
365
|
+
}
|
|
366
|
+
if (!res.ok) {
|
|
367
|
+
const text = await res.text();
|
|
368
|
+
throw new Error(`${method} ${path} failed (${res.status}): ${text.slice(0, 200)}`);
|
|
369
|
+
}
|
|
370
|
+
return res.json();
|
|
371
|
+
}
|
|
362
372
|
async _publicGet(path, params = {}) {
|
|
363
373
|
const qs = Object.entries(params)
|
|
364
374
|
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
365
375
|
.join("&");
|
|
366
376
|
const url = `${this._baseUrl}${path}${qs ? `?${qs}` : ""}`;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
377
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
378
|
+
const res = await fetch(url);
|
|
379
|
+
const result = await this._handleResponse(res, "GET", path, attempt);
|
|
380
|
+
if (result !== null)
|
|
381
|
+
return result;
|
|
371
382
|
}
|
|
372
|
-
|
|
383
|
+
throw new Error(`GET ${path} failed: max retries exceeded`);
|
|
373
384
|
}
|
|
374
385
|
async _signedGet(path, params = {}) {
|
|
375
386
|
if (this.isReadOnly)
|
|
376
387
|
throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
388
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
389
|
+
const signed = this._sign({ ...params });
|
|
390
|
+
const url = `${this._baseUrl}${path}?${signed}`;
|
|
391
|
+
const res = await fetch(url, {
|
|
392
|
+
headers: { "X-MBX-APIKEY": this._apiKey },
|
|
393
|
+
});
|
|
394
|
+
const result = await this._handleResponse(res, "GET", path, attempt);
|
|
395
|
+
if (result !== null)
|
|
396
|
+
return result;
|
|
385
397
|
}
|
|
386
|
-
|
|
398
|
+
throw new Error(`GET ${path} failed: max retries exceeded`);
|
|
387
399
|
}
|
|
388
400
|
async _signedPost(path, params = {}) {
|
|
389
401
|
if (this.isReadOnly)
|
|
390
402
|
throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
404
|
+
const signed = this._sign({ ...params });
|
|
405
|
+
const res = await fetch(`${this._baseUrl}${path}`, {
|
|
406
|
+
method: "POST",
|
|
407
|
+
headers: {
|
|
408
|
+
"X-MBX-APIKEY": this._apiKey,
|
|
409
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
410
|
+
},
|
|
411
|
+
body: signed,
|
|
412
|
+
});
|
|
413
|
+
const result = await this._handleResponse(res, "POST", path, attempt);
|
|
414
|
+
if (result !== null)
|
|
415
|
+
return result;
|
|
403
416
|
}
|
|
404
|
-
|
|
417
|
+
throw new Error(`POST ${path} failed: max retries exceeded`);
|
|
405
418
|
}
|
|
406
419
|
async _signedDelete(path, params = {}) {
|
|
407
420
|
if (this.isReadOnly)
|
|
408
421
|
throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
422
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
423
|
+
const signed = this._sign({ ...params });
|
|
424
|
+
const res = await fetch(`${this._baseUrl}${path}?${signed}`, {
|
|
425
|
+
method: "DELETE",
|
|
426
|
+
headers: { "X-MBX-APIKEY": this._apiKey },
|
|
427
|
+
});
|
|
428
|
+
const result = await this._handleResponse(res, "DELETE", path, attempt);
|
|
429
|
+
if (result !== null)
|
|
430
|
+
return result;
|
|
417
431
|
}
|
|
418
|
-
|
|
432
|
+
throw new Error(`DELETE ${path} failed: max retries exceeded`);
|
|
419
433
|
}
|
|
420
434
|
}
|