perp-cli 0.7.12 → 0.7.13
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/bot/engine.js +30 -23
- package/dist/bot/strategies/funding-arb-strategy.js +38 -0
- package/dist/exchanges/aster.js +1 -1
- package/dist/exchanges/hyperliquid.d.ts +4 -1
- package/dist/exchanges/hyperliquid.js +12 -5
- package/dist/exchanges/lighter.d.ts +3 -0
- package/dist/exchanges/lighter.js +9 -3
- package/dist/exchanges/pacifica.d.ts +3 -0
- package/dist/exchanges/pacifica.js +13 -6
- package/package.json +1 -1
package/dist/bot/engine.js
CHANGED
|
@@ -152,22 +152,6 @@ mode = "headless") {
|
|
|
152
152
|
try {
|
|
153
153
|
// Get market data (enriched for strategy use)
|
|
154
154
|
const snapshot = await getEnrichedSnapshot(adapter, config.symbol);
|
|
155
|
-
// Update equity
|
|
156
|
-
try {
|
|
157
|
-
const bal = await adapter.getBalance();
|
|
158
|
-
state.equity = parseFloat(bal.equity);
|
|
159
|
-
if (state.equity > state.peakEquity)
|
|
160
|
-
state.peakEquity = state.equity;
|
|
161
|
-
// Reset daily baseline at day boundary
|
|
162
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
163
|
-
if (today !== state.dailyStartDate) {
|
|
164
|
-
state.dailyStartEquity = state.equity;
|
|
165
|
-
state.dailyStartDate = today;
|
|
166
|
-
}
|
|
167
|
-
state.dailyPnl = state.equity - state.dailyStartEquity;
|
|
168
|
-
state.totalPnl = state.equity - state.initialEquity;
|
|
169
|
-
}
|
|
170
|
-
catch { /* non-critical */ }
|
|
171
155
|
const context = {
|
|
172
156
|
equity: state.equity,
|
|
173
157
|
startTime: state.startTime,
|
|
@@ -258,18 +242,41 @@ mode = "headless") {
|
|
|
258
242
|
effectiveLog(` Exit condition met, returning to monitoring...`);
|
|
259
243
|
}
|
|
260
244
|
}
|
|
261
|
-
// ──
|
|
245
|
+
// ── Fetch balances + emit state update for TUI / JSON ──
|
|
262
246
|
const [positions, openOrders, exchangeBalances] = await Promise.all([
|
|
263
247
|
extraAdapters ? fetchAllPositions(adapter, config.symbol, extraAdapters) : fetchPositions(adapter, config.symbol, adapter.name),
|
|
264
248
|
fetchOpenOrders(adapter, config.symbol),
|
|
265
|
-
|
|
249
|
+
fetchExchangeBalances(adapter, extraAdapters),
|
|
266
250
|
]);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
251
|
+
// Update equity using the already-fetched balance (avoids a redundant getBalance() call)
|
|
252
|
+
try {
|
|
253
|
+
const primaryBal = exchangeBalances.find(b => b.exchange === adapter.name);
|
|
254
|
+
if (primaryBal) {
|
|
255
|
+
state.equity = parseFloat(primaryBal.equity);
|
|
256
|
+
if (state.equity > state.peakEquity)
|
|
257
|
+
state.peakEquity = state.equity;
|
|
258
|
+
// Reset daily baseline at day boundary
|
|
259
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
260
|
+
if (today !== state.dailyStartDate) {
|
|
261
|
+
state.dailyStartEquity = state.equity;
|
|
262
|
+
state.dailyStartDate = today;
|
|
263
|
+
}
|
|
264
|
+
state.dailyPnl = state.equity - state.dailyStartEquity;
|
|
265
|
+
state.totalPnl = state.equity - state.initialEquity;
|
|
266
|
+
}
|
|
270
267
|
}
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
catch { /* non-critical */ }
|
|
269
|
+
if (mode === "tui" || mode === "json") {
|
|
270
|
+
const tuiState = buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx, exchangeBalances);
|
|
271
|
+
if (mode === "tui") {
|
|
272
|
+
tuiEmitter.emitState(tuiState);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.log(JSON.stringify(tuiState));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
effectiveLog(` tick: phase=${state.phase} equity=$${state.equity.toFixed(2)}`);
|
|
273
280
|
}
|
|
274
281
|
// Update job state
|
|
275
282
|
if (jobId) {
|
|
@@ -26,6 +26,9 @@ export class FundingArbStrategy {
|
|
|
26
26
|
ctx.state.set("arbRunning", true);
|
|
27
27
|
ctx.state.set("arbOpenPositions", []);
|
|
28
28
|
ctx.state.set("arbPositions", 0);
|
|
29
|
+
ctx.state.set("fundingIncome", 0);
|
|
30
|
+
ctx.state.set("fundingLastCheck", Date.now());
|
|
31
|
+
ctx.state.set("fundingHistory", []);
|
|
29
32
|
ctx.log(` [ARB] Funding arb ready | spread >= ${params.min_spread}% | close < ${params.close_spread}% | size $${params.size_usd}`);
|
|
30
33
|
ctx.log(` [ARB] Exchanges: ${params.exchanges.join(", ")}`);
|
|
31
34
|
}
|
|
@@ -54,6 +57,41 @@ export class FundingArbStrategy {
|
|
|
54
57
|
map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
|
|
55
58
|
ratesByExchange.set(name, map);
|
|
56
59
|
}
|
|
60
|
+
// ── Fetch funding income periodically (every 5 minutes) ──
|
|
61
|
+
const lastCheck = ctx.state.get("fundingLastCheck");
|
|
62
|
+
if (openPositions.length > 0 && Date.now() - lastCheck > 5 * 60 * 1000) {
|
|
63
|
+
try {
|
|
64
|
+
let totalFunding = 0;
|
|
65
|
+
for (const [name, a] of adapters) {
|
|
66
|
+
try {
|
|
67
|
+
const payments = await a.getFundingPayments(50);
|
|
68
|
+
const startTime = ctx.state.get("fundingLastCheck");
|
|
69
|
+
const recent = payments.filter(p => p.timestamp > startTime - 5 * 60 * 1000);
|
|
70
|
+
const sum = recent.reduce((s, p) => s + parseFloat(p.amount), 0);
|
|
71
|
+
totalFunding += sum;
|
|
72
|
+
if (Math.abs(sum) > 0.001) {
|
|
73
|
+
ctx.log(` [FUND] ${name}: $${sum.toFixed(4)} funding received`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { /* not all adapters support this */ }
|
|
77
|
+
}
|
|
78
|
+
const prev = ctx.state.get("fundingIncome");
|
|
79
|
+
ctx.state.set("fundingIncome", prev + totalFunding);
|
|
80
|
+
ctx.state.set("fundingLastCheck", Date.now());
|
|
81
|
+
}
|
|
82
|
+
catch { /* non-critical */ }
|
|
83
|
+
}
|
|
84
|
+
// ── Update strategy state for TUI display ──
|
|
85
|
+
const arbPosForDisplay = openPositions.map(p => {
|
|
86
|
+
const longRate = ratesByExchange.get(p.longExchange)?.get(p.symbol)?.rate;
|
|
87
|
+
const shortRate = ratesByExchange.get(p.shortExchange)?.get(p.symbol)?.rate;
|
|
88
|
+
const spread = (longRate !== undefined && shortRate !== undefined)
|
|
89
|
+
? computeAnnualSpread(shortRate, p.shortExchange, longRate, p.longExchange)
|
|
90
|
+
: 0;
|
|
91
|
+
return `${p.symbol} ${p.longExchange}↔${p.shortExchange} ${spread.toFixed(1)}%`;
|
|
92
|
+
});
|
|
93
|
+
ctx.state.set("arbPositionDetails", arbPosForDisplay);
|
|
94
|
+
ctx.state.set("fundingTotal", `$${ctx.state.get("fundingIncome").toFixed(4)}`);
|
|
57
95
|
// ── Check existing positions for close_spread ──
|
|
58
96
|
const toClose = [];
|
|
59
97
|
for (const pos of openPositions) {
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -14,7 +14,7 @@ export class AsterAdapter {
|
|
|
14
14
|
_testnet;
|
|
15
15
|
_marketsCache = null;
|
|
16
16
|
_marketsCacheTime = 0;
|
|
17
|
-
static CACHE_TTL =
|
|
17
|
+
static CACHE_TTL = 30_000; // 30 seconds
|
|
18
18
|
constructor(apiKey, apiSecret, testnet = false) {
|
|
19
19
|
this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
|
|
20
20
|
this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
|
|
@@ -16,6 +16,9 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
|
|
|
16
16
|
/** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
|
|
17
17
|
private _dex;
|
|
18
18
|
private static _lastNonce;
|
|
19
|
+
private _marketsCache;
|
|
20
|
+
private _marketsCacheTime;
|
|
21
|
+
private static readonly CACHE_TTL;
|
|
19
22
|
constructor(privateKey?: string, testnet?: boolean);
|
|
20
23
|
/** Set the HIP-3 deployed perp dex to query/trade on. */
|
|
21
24
|
setDex(dex: string): void;
|
|
@@ -44,7 +47,7 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
|
|
|
44
47
|
asks: [string, string][];
|
|
45
48
|
}>;
|
|
46
49
|
private ensureAddress;
|
|
47
|
-
/**
|
|
50
|
+
/** Read-through cached clearinghouseState — returns cached data if fresh */
|
|
48
51
|
private _getClearinghouseState;
|
|
49
52
|
/** Cached spot clearinghouse state — shared between getBalance() and HyperliquidSpotAdapter */
|
|
50
53
|
_getSpotClearinghouseState(): Promise<Record<string, unknown>>;
|
|
@@ -15,7 +15,9 @@ export class HyperliquidAdapter {
|
|
|
15
15
|
/** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
|
|
16
16
|
_dex = "";
|
|
17
17
|
static _lastNonce = 0;
|
|
18
|
-
|
|
18
|
+
_marketsCache = null;
|
|
19
|
+
_marketsCacheTime = 0;
|
|
20
|
+
static CACHE_TTL = 30_000;
|
|
19
21
|
constructor(privateKey, testnet = false) {
|
|
20
22
|
// Disable WebSocket in SDK — we use REST for all info calls and raw
|
|
21
23
|
// fetch for /exchange. This avoids the false "Please install ws" warning
|
|
@@ -181,6 +183,8 @@ export class HyperliquidAdapter {
|
|
|
181
183
|
throw new Error(`Unknown asset: ${symbol}`);
|
|
182
184
|
}
|
|
183
185
|
async getMarkets() {
|
|
186
|
+
if (this._marketsCache && Date.now() - this._marketsCacheTime < HyperliquidAdapter.CACHE_TTL)
|
|
187
|
+
return this._marketsCache;
|
|
184
188
|
let universe;
|
|
185
189
|
let ctxs;
|
|
186
190
|
let mids = {};
|
|
@@ -207,7 +211,7 @@ export class HyperliquidAdapter {
|
|
|
207
211
|
this._assetMapReverse.set(i, sym);
|
|
208
212
|
});
|
|
209
213
|
}
|
|
210
|
-
|
|
214
|
+
const result = universe.map((asset, i) => {
|
|
211
215
|
const ctx = (ctxs[i] ?? {});
|
|
212
216
|
const sym = String(asset.name);
|
|
213
217
|
return {
|
|
@@ -220,6 +224,9 @@ export class HyperliquidAdapter {
|
|
|
220
224
|
maxLeverage: Number(asset.maxLeverage ?? 50),
|
|
221
225
|
};
|
|
222
226
|
});
|
|
227
|
+
this._marketsCache = result;
|
|
228
|
+
this._marketsCacheTime = Date.now();
|
|
229
|
+
return result;
|
|
223
230
|
}
|
|
224
231
|
async getOrderbook(symbol) {
|
|
225
232
|
let levels;
|
|
@@ -256,12 +263,12 @@ export class HyperliquidAdapter {
|
|
|
256
263
|
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
257
264
|
}
|
|
258
265
|
}
|
|
259
|
-
/**
|
|
266
|
+
/** Read-through cached clearinghouseState — returns cached data if fresh */
|
|
260
267
|
async _getClearinghouseState() {
|
|
261
268
|
this.ensureAddress();
|
|
262
|
-
const {
|
|
269
|
+
const { withCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
263
270
|
const key = this._dex ? `acct:hl:chs:${this._address}:${this._dex}` : `acct:hl:chs:${this._address}`;
|
|
264
|
-
return
|
|
271
|
+
return withCache(key, TTL_ACCOUNT, async () => {
|
|
265
272
|
const state = this._dex
|
|
266
273
|
? await this._infoPost({ type: "clearinghouseState", user: this._address, dex: this._dex })
|
|
267
274
|
: await this.sdk.info.perpetuals.getClearinghouseState(this._address);
|
|
@@ -20,6 +20,9 @@ export declare class LighterAdapter implements ExchangeAdapter {
|
|
|
20
20
|
private _testnet;
|
|
21
21
|
private _readOnly;
|
|
22
22
|
private _evmSigner?;
|
|
23
|
+
private _marketsCache;
|
|
24
|
+
private _marketsCacheTime;
|
|
25
|
+
private static readonly CACHE_TTL;
|
|
23
26
|
/**
|
|
24
27
|
* @param evmKey EVM private key (0x-prefixed, 32 bytes) — for deposits & key registration
|
|
25
28
|
* @param testnet Use testnet (chain ID 300) instead of mainnet (chain ID 304)
|
|
@@ -18,7 +18,9 @@ export class LighterAdapter {
|
|
|
18
18
|
_testnet;
|
|
19
19
|
_readOnly;
|
|
20
20
|
_evmSigner;
|
|
21
|
-
|
|
21
|
+
_marketsCache = null;
|
|
22
|
+
_marketsCacheTime = 0;
|
|
23
|
+
static CACHE_TTL = 30_000;
|
|
22
24
|
/**
|
|
23
25
|
* @param evmKey EVM private key (0x-prefixed, 32 bytes) — for deposits & key registration
|
|
24
26
|
* @param testnet Use testnet (chain ID 300) instead of mainnet (chain ID 304)
|
|
@@ -260,6 +262,8 @@ export class LighterAdapter {
|
|
|
260
262
|
return { baseAmount, priceTicks };
|
|
261
263
|
}
|
|
262
264
|
async getMarkets() {
|
|
265
|
+
if (this._marketsCache && Date.now() - this._marketsCacheTime < LighterAdapter.CACHE_TTL)
|
|
266
|
+
return this._marketsCache;
|
|
263
267
|
const markets = [];
|
|
264
268
|
try {
|
|
265
269
|
const res = await this.restGet("/orderBookDetails", {});
|
|
@@ -281,13 +285,15 @@ export class LighterAdapter {
|
|
|
281
285
|
}
|
|
282
286
|
}
|
|
283
287
|
catch { /* non-critical */ }
|
|
288
|
+
this._marketsCache = markets;
|
|
289
|
+
this._marketsCacheTime = Date.now();
|
|
284
290
|
return markets;
|
|
285
291
|
}
|
|
286
292
|
async fetchAccount() {
|
|
287
293
|
if (!this._address)
|
|
288
294
|
return null;
|
|
289
|
-
const {
|
|
290
|
-
return
|
|
295
|
+
const { withCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
296
|
+
return withCache(`acct:lt:account:${this._address}`, TTL_ACCOUNT, async () => {
|
|
291
297
|
const res = await this.restGet("/account", {
|
|
292
298
|
by: "l1_address",
|
|
293
299
|
value: this._address,
|
|
@@ -11,6 +11,9 @@ export declare class PacificaAdapter implements ExchangeAdapter {
|
|
|
11
11
|
private _hasRealKey;
|
|
12
12
|
private account;
|
|
13
13
|
private signMessage;
|
|
14
|
+
private _marketsCache;
|
|
15
|
+
private _marketsCacheTime;
|
|
16
|
+
private static readonly CACHE_TTL;
|
|
14
17
|
constructor(keypair: Keypair, network?: Network, builderCode?: string, hasRealKey?: boolean);
|
|
15
18
|
private ensureSigner;
|
|
16
19
|
/** Inject an external Solana signer. */
|
|
@@ -9,7 +9,9 @@ export class PacificaAdapter {
|
|
|
9
9
|
_hasRealKey;
|
|
10
10
|
account;
|
|
11
11
|
signMessage;
|
|
12
|
-
|
|
12
|
+
_marketsCache = null;
|
|
13
|
+
_marketsCacheTime = 0;
|
|
14
|
+
static CACHE_TTL = 30_000;
|
|
13
15
|
constructor(keypair, network = "mainnet", builderCode, hasRealKey = true) {
|
|
14
16
|
this._solanaSigner = new LocalSolanaSigner(keypair);
|
|
15
17
|
this._hasRealKey = hasRealKey;
|
|
@@ -36,12 +38,12 @@ export class PacificaAdapter {
|
|
|
36
38
|
throw new Error("keypair not available with external signer");
|
|
37
39
|
}
|
|
38
40
|
async _getPrices() {
|
|
39
|
-
const {
|
|
40
|
-
return
|
|
41
|
+
const { withCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
42
|
+
return withCache(`acct:pac:prices:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPrices());
|
|
41
43
|
}
|
|
42
44
|
async _getPositions() {
|
|
43
|
-
const {
|
|
44
|
-
return
|
|
45
|
+
const { withCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
46
|
+
return withCache(`acct:pac:positions:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPositions(this.account));
|
|
45
47
|
}
|
|
46
48
|
get publicKey() {
|
|
47
49
|
return this.account;
|
|
@@ -53,12 +55,14 @@ export class PacificaAdapter {
|
|
|
53
55
|
return this.signMessage;
|
|
54
56
|
}
|
|
55
57
|
async getMarkets() {
|
|
58
|
+
if (this._marketsCache && Date.now() - this._marketsCacheTime < PacificaAdapter.CACHE_TTL)
|
|
59
|
+
return this._marketsCache;
|
|
56
60
|
const [markets, prices] = await Promise.all([
|
|
57
61
|
this.client.getInfo(),
|
|
58
62
|
this._getPrices(),
|
|
59
63
|
]);
|
|
60
64
|
const priceMap = new Map(prices.map((p) => [p.symbol, p]));
|
|
61
|
-
|
|
65
|
+
const result = markets.map((m) => {
|
|
62
66
|
const p = priceMap.get(m.symbol);
|
|
63
67
|
return {
|
|
64
68
|
symbol: m.symbol,
|
|
@@ -70,6 +74,9 @@ export class PacificaAdapter {
|
|
|
70
74
|
maxLeverage: m.max_leverage,
|
|
71
75
|
};
|
|
72
76
|
});
|
|
77
|
+
this._marketsCache = result;
|
|
78
|
+
this._marketsCacheTime = Date.now();
|
|
79
|
+
return result;
|
|
73
80
|
}
|
|
74
81
|
async getOrderbook(symbol) {
|
|
75
82
|
const book = await this.client.getBook(symbol);
|