perp-cli 0.7.13 → 0.7.16
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 +70 -2
- package/dist/bot/strategies/funding-arb-strategy.js +66 -1
- package/dist/bot/tui/BotDashboard.js +13 -3
- package/dist/exchanges/aster.d.ts +4 -0
- package/dist/exchanges/aster.js +23 -4
- package/dist/exchanges/hyperliquid-spot.js +1 -0
- package/dist/exchanges/spot-interface.d.ts +1 -0
- package/package.json +1 -1
package/dist/bot/engine.js
CHANGED
|
@@ -454,6 +454,71 @@ async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
|
|
|
454
454
|
}));
|
|
455
455
|
return results;
|
|
456
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
|
+
const total = Number(b.total);
|
|
481
|
+
const entryNtl = b.entryNtl ? Number(b.entryNtl) : 0;
|
|
482
|
+
const avgEntry = (entryNtl > 0 && total > 0) ? (entryNtl / total) : 0;
|
|
483
|
+
const mark = Number(markPrice);
|
|
484
|
+
const uPnL = avgEntry > 0 ? (mark - avgEntry) * total : 0;
|
|
485
|
+
hlPositions.push({
|
|
486
|
+
symbol: base, side: "spot", size: b.total,
|
|
487
|
+
entryPrice: avgEntry > 0 ? avgEntry.toPrecision(5) : "—",
|
|
488
|
+
markPrice,
|
|
489
|
+
unrealizedPnl: avgEntry > 0 ? uPnL.toFixed(2) : "—",
|
|
490
|
+
exchange: adapter.name,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return hlPositions;
|
|
494
|
+
}
|
|
495
|
+
if (name === "lighter") {
|
|
496
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
497
|
+
const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
|
|
498
|
+
if (!(adapter instanceof LighterAdapter))
|
|
499
|
+
return [];
|
|
500
|
+
const ltSpot = new LighterSpotAdapter(adapter);
|
|
501
|
+
await ltSpot.init();
|
|
502
|
+
const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
|
|
503
|
+
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), m.markPrice]));
|
|
504
|
+
const ltPositions = [];
|
|
505
|
+
for (const b of raw) {
|
|
506
|
+
if (Number(b.total) <= 0)
|
|
507
|
+
continue;
|
|
508
|
+
const base = b.token.toUpperCase();
|
|
509
|
+
const markPrice = priceMap.get(base) ?? "0";
|
|
510
|
+
if (base === "USDC" || base === "USDC_SPOT" || markPrice === "0")
|
|
511
|
+
continue;
|
|
512
|
+
ltPositions.push({ symbol: base, side: "spot", size: b.total, entryPrice: "—", markPrice, unrealizedPnl: "—", exchange: adapter.name });
|
|
513
|
+
}
|
|
514
|
+
return ltPositions;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Spot adapter unavailable — skip silently
|
|
519
|
+
}
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
457
522
|
async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
|
|
458
523
|
const adapters = [[primaryAdapter.name, primaryAdapter]];
|
|
459
524
|
if (extraAdapters) {
|
|
@@ -462,8 +527,11 @@ async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
|
|
|
462
527
|
}
|
|
463
528
|
const all = [];
|
|
464
529
|
await Promise.all(adapters.map(async ([name, a]) => {
|
|
465
|
-
const
|
|
466
|
-
|
|
530
|
+
const [perpPos, spotPos] = await Promise.all([
|
|
531
|
+
fetchPositions(a, symbol, name),
|
|
532
|
+
fetchSpotPositions(a),
|
|
533
|
+
]);
|
|
534
|
+
all.push([...perpPos, ...spotPos]);
|
|
467
535
|
}));
|
|
468
536
|
return all.flat();
|
|
469
537
|
}
|
|
@@ -31,6 +31,71 @@ export class FundingArbStrategy {
|
|
|
31
31
|
ctx.state.set("fundingHistory", []);
|
|
32
32
|
ctx.log(` [ARB] Funding arb ready | spread >= ${params.min_spread}% | close < ${params.close_spread}% | size $${params.size_usd}`);
|
|
33
33
|
ctx.log(` [ARB] Exchanges: ${params.exchanges.join(", ")}`);
|
|
34
|
+
// ── Recover existing positions from exchanges ──
|
|
35
|
+
try {
|
|
36
|
+
const extraAdapters = ctx.state.get("extraAdapters");
|
|
37
|
+
const adapters = new Map();
|
|
38
|
+
adapters.set(ctx.adapter.name.toLowerCase(), ctx.adapter);
|
|
39
|
+
if (extraAdapters) {
|
|
40
|
+
for (const [name, a] of extraAdapters)
|
|
41
|
+
adapters.set(name, a);
|
|
42
|
+
}
|
|
43
|
+
// Fetch all positions across exchanges
|
|
44
|
+
const positionsByExchange = new Map();
|
|
45
|
+
for (const [name, a] of adapters) {
|
|
46
|
+
try {
|
|
47
|
+
const positions = await a.getPositions();
|
|
48
|
+
positionsByExchange.set(name, positions.map(p => ({
|
|
49
|
+
symbol: p.symbol.toUpperCase(),
|
|
50
|
+
side: p.side,
|
|
51
|
+
size: p.size,
|
|
52
|
+
})));
|
|
53
|
+
}
|
|
54
|
+
catch { /* skip unavailable exchange */ }
|
|
55
|
+
}
|
|
56
|
+
// Match long/short pairs across exchanges → reconstruct arbOpenPositions
|
|
57
|
+
const recovered = [];
|
|
58
|
+
const used = new Set(); // "exchange:symbol" to avoid double-matching
|
|
59
|
+
for (const [exA, posA] of positionsByExchange) {
|
|
60
|
+
for (const pA of posA) {
|
|
61
|
+
const keyA = `${exA}:${pA.symbol}`;
|
|
62
|
+
if (used.has(keyA))
|
|
63
|
+
continue;
|
|
64
|
+
for (const [exB, posB] of positionsByExchange) {
|
|
65
|
+
if (exA === exB)
|
|
66
|
+
continue;
|
|
67
|
+
for (const pB of posB) {
|
|
68
|
+
const keyB = `${exB}:${pB.symbol}`;
|
|
69
|
+
if (used.has(keyB))
|
|
70
|
+
continue;
|
|
71
|
+
if (pA.symbol !== pB.symbol)
|
|
72
|
+
continue;
|
|
73
|
+
if (pA.side === pB.side)
|
|
74
|
+
continue;
|
|
75
|
+
// Found a matching pair
|
|
76
|
+
const longEx = pA.side === "long" ? exA : exB;
|
|
77
|
+
const shortEx = pA.side === "short" ? exA : exB;
|
|
78
|
+
const size = pA.side === "long" ? pA.size : pB.size;
|
|
79
|
+
recovered.push({ symbol: pA.symbol, longExchange: longEx, shortExchange: shortEx, entrySpread: 0, size });
|
|
80
|
+
used.add(keyA);
|
|
81
|
+
used.add(keyB);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (used.has(keyA))
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (recovered.length > 0) {
|
|
90
|
+
ctx.state.set("arbOpenPositions", recovered);
|
|
91
|
+
ctx.state.set("arbPositions", recovered.length);
|
|
92
|
+
ctx.log(` [ARB] Recovered ${recovered.length} existing position(s): ${recovered.map(p => `${p.symbol} ${p.longExchange}↔${p.shortExchange}`).join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
97
|
+
ctx.log(` [ARB] Position recovery failed: ${msg}`);
|
|
98
|
+
}
|
|
34
99
|
}
|
|
35
100
|
async onTick(ctx, _snapshot) {
|
|
36
101
|
const params = this.params;
|
|
@@ -59,7 +124,7 @@ export class FundingArbStrategy {
|
|
|
59
124
|
}
|
|
60
125
|
// ── Fetch funding income periodically (every 5 minutes) ──
|
|
61
126
|
const lastCheck = ctx.state.get("fundingLastCheck");
|
|
62
|
-
if (
|
|
127
|
+
if (Date.now() - lastCheck > 5 * 60 * 1000) {
|
|
63
128
|
try {
|
|
64
129
|
let totalFunding = 0;
|
|
65
130
|
for (const [name, a] of adapters) {
|
|
@@ -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 ──
|
|
@@ -72,8 +72,18 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
|
|
|
72
72
|
const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
|
|
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
|
-
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) }),
|
|
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))))] })] }),
|
|
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) }), _jsx(Text, { color: isNaN(upnl) ? "gray" : pnlColor(upnl), children: isNaN(upnl) ? "$—" : `$${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))))] })] }), _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
|
|
@@ -160,6 +160,7 @@ export class HyperliquidSpotAdapter {
|
|
|
160
160
|
total: String(b.total ?? "0"),
|
|
161
161
|
available: String(Number(b.total ?? 0) - Number(b.hold ?? 0)),
|
|
162
162
|
held: String(b.hold ?? "0"),
|
|
163
|
+
entryNtl: b.entryNtl !== undefined ? String(b.entryNtl) : undefined,
|
|
163
164
|
}));
|
|
164
165
|
}
|
|
165
166
|
catch {
|