perp-cli 0.7.12 → 0.7.15
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 +89 -25
- package/dist/bot/strategies/funding-arb-strategy.js +38 -0
- package/dist/bot/tui/BotDashboard.js +12 -2
- package/dist/exchanges/aster.d.ts +4 -0
- package/dist/exchanges/aster.js +23 -4
- 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
|
+
}
|
|
267
|
+
}
|
|
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
|
+
}
|
|
270
277
|
}
|
|
271
|
-
else
|
|
272
|
-
|
|
278
|
+
else {
|
|
279
|
+
effectiveLog(` tick: phase=${state.phase} equity=$${state.equity.toFixed(2)}`);
|
|
273
280
|
}
|
|
274
281
|
// Update job state
|
|
275
282
|
if (jobId) {
|
|
@@ -447,6 +454,60 @@ async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
|
|
|
447
454
|
}));
|
|
448
455
|
return results;
|
|
449
456
|
}
|
|
457
|
+
async function fetchSpotPositions(adapter) {
|
|
458
|
+
const name = adapter.name.toLowerCase();
|
|
459
|
+
if (name !== "hyperliquid" && name !== "lighter")
|
|
460
|
+
return [];
|
|
461
|
+
try {
|
|
462
|
+
if (name === "hyperliquid") {
|
|
463
|
+
const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
|
|
464
|
+
const { HyperliquidAdapter } = await import("../exchanges/hyperliquid.js");
|
|
465
|
+
if (!(adapter instanceof HyperliquidAdapter))
|
|
466
|
+
return [];
|
|
467
|
+
const hlSpot = new HyperliquidSpotAdapter(adapter);
|
|
468
|
+
await hlSpot.init();
|
|
469
|
+
const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
|
|
470
|
+
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), m.markPrice]));
|
|
471
|
+
const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
|
|
472
|
+
const hlPositions = [];
|
|
473
|
+
for (const b of raw) {
|
|
474
|
+
if (Number(b.total) <= 0)
|
|
475
|
+
continue;
|
|
476
|
+
const base = strip(b.token);
|
|
477
|
+
const markPrice = priceMap.get(base) ?? "0";
|
|
478
|
+
if (base === "USDC" || markPrice === "0")
|
|
479
|
+
continue;
|
|
480
|
+
hlPositions.push({ symbol: base, side: "spot", size: b.total, entryPrice: "—", markPrice, unrealizedPnl: "—", exchange: adapter.name });
|
|
481
|
+
}
|
|
482
|
+
return hlPositions;
|
|
483
|
+
}
|
|
484
|
+
if (name === "lighter") {
|
|
485
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
486
|
+
const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
|
|
487
|
+
if (!(adapter instanceof LighterAdapter))
|
|
488
|
+
return [];
|
|
489
|
+
const ltSpot = new LighterSpotAdapter(adapter);
|
|
490
|
+
await ltSpot.init();
|
|
491
|
+
const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
|
|
492
|
+
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), m.markPrice]));
|
|
493
|
+
const ltPositions = [];
|
|
494
|
+
for (const b of raw) {
|
|
495
|
+
if (Number(b.total) <= 0)
|
|
496
|
+
continue;
|
|
497
|
+
const base = b.token.toUpperCase();
|
|
498
|
+
const markPrice = priceMap.get(base) ?? "0";
|
|
499
|
+
if (base === "USDC" || base === "USDC_SPOT" || markPrice === "0")
|
|
500
|
+
continue;
|
|
501
|
+
ltPositions.push({ symbol: base, side: "spot", size: b.total, entryPrice: "—", markPrice, unrealizedPnl: "—", exchange: adapter.name });
|
|
502
|
+
}
|
|
503
|
+
return ltPositions;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Spot adapter unavailable — skip silently
|
|
508
|
+
}
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
450
511
|
async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
|
|
451
512
|
const adapters = [[primaryAdapter.name, primaryAdapter]];
|
|
452
513
|
if (extraAdapters) {
|
|
@@ -455,8 +516,11 @@ async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
|
|
|
455
516
|
}
|
|
456
517
|
const all = [];
|
|
457
518
|
await Promise.all(adapters.map(async ([name, a]) => {
|
|
458
|
-
const
|
|
459
|
-
|
|
519
|
+
const [perpPos, spotPos] = await Promise.all([
|
|
520
|
+
fetchPositions(a, symbol, name),
|
|
521
|
+
fetchSpotPositions(a),
|
|
522
|
+
]);
|
|
523
|
+
all.push([...perpPos, ...spotPos]);
|
|
460
524
|
}));
|
|
461
525
|
return all.flat();
|
|
462
526
|
}
|
|
@@ -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) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useCallback } from "react";
|
|
3
3
|
import { Box, Text, useApp, useInput } from "ink";
|
|
4
4
|
// ── Helpers ──
|
|
@@ -73,7 +73,17 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
|
|
|
73
73
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", (state.exchangeBalances?.reduce((s, e) => s + parseFloat(e.equity), 0) ?? state.equity).toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsxs(Text, { color: "white", children: [state.volatility24h.toFixed(1), "%"] })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.equity.toFixed(2)] }), _jsxs(Text, { color: "gray", children: [" (peak: $", state.peakEquity.toFixed(2), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsxs(Text, { bold: true, color: drawdown > 0 ? "red" : "green", children: ["$", drawdown.toFixed(2), " (", drawdownPct.toFixed(1), "%)"] })] }), _jsxs(Text, { children: ["Fills: ", _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), _jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsxs(Text, { color: "gray", children: ["$", parseFloat(eb.available).toFixed(2)] })] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), state.positions.length === 0 ? (_jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
|
|
74
74
|
const upnl = parseFloat(p.unrealizedPnl);
|
|
75
75
|
return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsxs(Text, { color: pnlColor(upnl), children: ["$", upnl.toFixed(2)] })] }, i));
|
|
76
|
-
})] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] }),
|
|
76
|
+
})] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: state.strategyState.fundingTotal !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Funding & Arb" }), _jsxs(Box, { flexDirection: "row", marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Funding Income: " }), _jsx(Text, { color: "white", children: String(state.strategyState.fundingTotal) }), _jsx(Text, { color: "gray", children: " Positions: " }), _jsx(Text, { color: "white", children: String(state.strategyState.arbPositions ?? 0) })] }), Array.isArray(state.strategyState.arbPositionDetails) &&
|
|
77
|
+
state.strategyState.arbPositionDetails.map((detail, i) => (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "yellow", children: detail }) }, i)))] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Strategy State" }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [Object.entries(state.strategyState)
|
|
78
|
+
.filter(([k, v]) => {
|
|
79
|
+
if (k === 'arbRunning' || k === 'fundingLastCheck' || k === 'fundingHistory' || k === 'arbOpenPositions')
|
|
80
|
+
return false;
|
|
81
|
+
if (Array.isArray(v) || (typeof v === 'object' && v !== null))
|
|
82
|
+
return false;
|
|
83
|
+
return true;
|
|
84
|
+
})
|
|
85
|
+
.slice(0, 12)
|
|
86
|
+
.map(([k, v]) => (_jsxs(Box, { marginRight: 2, children: [_jsxs(Text, { color: "gray", children: [k, ": "] }), _jsx(Text, { color: "white", children: String(v) })] }, k))), Object.keys(state.strategyState).length === 0 && (_jsx(Text, { color: "gray", children: " --" }))] })] })) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: MAX_LOG_LINES + 2, children: [_jsx(Text, { color: "cyan", bold: true, children: "Log" }), logs.length === 0 ? (_jsx(Text, { color: "gray", children: " Waiting for events..." })) : (logs.map((entry, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "gray", children: [entry.time, " "] }), _jsx(Text, { children: colorizeLog(entry.message) })] }, i))))] }), _jsx(Box, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: _jsxs(Text, { color: "gray", children: ["[", _jsx(Text, { color: "white", bold: true, children: "Q" }), "]uit [", _jsx(Text, { color: "white", bold: true, children: "P" }), "]", paused ? "resume" : "ause"] }) })] }));
|
|
77
87
|
}
|
|
78
88
|
// ── Utility ──
|
|
79
89
|
function pad(s, width) {
|
|
@@ -14,7 +14,11 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
14
14
|
private _testnet;
|
|
15
15
|
private _marketsCache;
|
|
16
16
|
private _marketsCacheTime;
|
|
17
|
+
private _accountCache;
|
|
18
|
+
private _positionsCache;
|
|
19
|
+
private _ordersCache;
|
|
17
20
|
private static readonly CACHE_TTL;
|
|
21
|
+
private static readonly ACCOUNT_CACHE_TTL;
|
|
18
22
|
constructor(apiKey?: string, apiSecret?: string, testnet?: boolean);
|
|
19
23
|
get isReadOnly(): boolean;
|
|
20
24
|
/** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -14,7 +14,11 @@ export class AsterAdapter {
|
|
|
14
14
|
_testnet;
|
|
15
15
|
_marketsCache = null;
|
|
16
16
|
_marketsCacheTime = 0;
|
|
17
|
-
|
|
17
|
+
_accountCache = null;
|
|
18
|
+
_positionsCache = null;
|
|
19
|
+
_ordersCache = null;
|
|
20
|
+
static CACHE_TTL = 30_000; // 30 seconds (markets)
|
|
21
|
+
static ACCOUNT_CACHE_TTL = 5_000; // 5 seconds (account/positions/orders)
|
|
18
22
|
constructor(apiKey, apiSecret, testnet = false) {
|
|
19
23
|
this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
|
|
20
24
|
this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
|
|
@@ -137,21 +141,29 @@ export class AsterAdapter {
|
|
|
137
141
|
}
|
|
138
142
|
// ── Account ──
|
|
139
143
|
async getBalance() {
|
|
144
|
+
if (this._accountCache && Date.now() - this._accountCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
|
|
145
|
+
return this._accountCache.data;
|
|
146
|
+
}
|
|
140
147
|
const account = await this._signedGet("/fapi/v2/account");
|
|
141
148
|
const totalWallet = Number(account.totalWalletBalance ?? 0);
|
|
142
149
|
const unrealizedPnl = Number(account.totalUnrealizedProfit ?? 0);
|
|
143
150
|
const available = Number(account.availableBalance ?? 0);
|
|
144
151
|
const marginUsed = Number(account.totalInitialMargin ?? 0);
|
|
145
|
-
|
|
152
|
+
const result = {
|
|
146
153
|
equity: String(totalWallet + unrealizedPnl),
|
|
147
154
|
available: String(available),
|
|
148
155
|
marginUsed: String(marginUsed),
|
|
149
156
|
unrealizedPnl: String(unrealizedPnl),
|
|
150
157
|
};
|
|
158
|
+
this._accountCache = { data: result, time: Date.now() };
|
|
159
|
+
return result;
|
|
151
160
|
}
|
|
152
161
|
async getPositions() {
|
|
162
|
+
if (this._positionsCache && Date.now() - this._positionsCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
|
|
163
|
+
return this._positionsCache.data;
|
|
164
|
+
}
|
|
153
165
|
const data = await this._signedGet("/fapi/v2/positionRisk");
|
|
154
|
-
|
|
166
|
+
const result = (data ?? [])
|
|
155
167
|
.filter((p) => Number(p.positionAmt ?? 0) !== 0)
|
|
156
168
|
.map((p) => {
|
|
157
169
|
const amt = Number(p.positionAmt ?? 0);
|
|
@@ -166,10 +178,15 @@ export class AsterAdapter {
|
|
|
166
178
|
leverage: Number(p.leverage ?? 1),
|
|
167
179
|
};
|
|
168
180
|
});
|
|
181
|
+
this._positionsCache = { data: result, time: Date.now() };
|
|
182
|
+
return result;
|
|
169
183
|
}
|
|
170
184
|
async getOpenOrders() {
|
|
185
|
+
if (this._ordersCache && Date.now() - this._ordersCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
|
|
186
|
+
return this._ordersCache.data;
|
|
187
|
+
}
|
|
171
188
|
const orders = await this._signedGet("/fapi/v1/openOrders");
|
|
172
|
-
|
|
189
|
+
const result = (orders ?? []).map((o) => ({
|
|
173
190
|
orderId: String(o.orderId ?? ""),
|
|
174
191
|
symbol: this._fromApi(String(o.symbol ?? "")),
|
|
175
192
|
side: String(o.side).toLowerCase(),
|
|
@@ -179,6 +196,8 @@ export class AsterAdapter {
|
|
|
179
196
|
status: String(o.status ?? ""),
|
|
180
197
|
type: String(o.type ?? ""),
|
|
181
198
|
}));
|
|
199
|
+
this._ordersCache = { data: result, time: Date.now() };
|
|
200
|
+
return result;
|
|
182
201
|
}
|
|
183
202
|
async getOrderHistory(limit = 30) {
|
|
184
203
|
// allOrders requires symbol — get from positions or recent trades
|
|
@@ -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);
|