perp-cli 0.14.0 → 0.14.2
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.js +29 -7
- package/dist/bot/conditions.d.ts +5 -1
- package/dist/bot/conditions.js +16 -11
- package/dist/bot/engine.d.ts +12 -0
- package/dist/bot/engine.js +55 -44
- package/dist/bot/strategies/claude-agent.js +2 -2
- package/dist/bot/strategies/funding-arb-strategy.js +16 -20
- package/dist/bot/strategies/funding-arb-utils.d.ts +19 -0
- package/dist/bot/strategies/funding-arb-utils.js +44 -0
- package/dist/bot/strategies/funding-auto.js +61 -37
- package/dist/bot/strategies/grid-strategy.d.ts +2 -0
- package/dist/bot/strategies/grid-strategy.js +21 -2
- package/dist/bot/strategies/spot-perp-arb-strategy.js +28 -20
- package/dist/bot/tui/BotDashboard.d.ts +13 -2
- package/dist/bot/tui/BotDashboard.js +8 -3
- package/dist/bridge-engine.d.ts +2 -0
- package/dist/bridge-engine.js +68 -31
- package/dist/cli-program.js +19 -2
- package/dist/commands/account.js +2 -2
- package/dist/commands/arb/index.d.ts +20 -0
- package/dist/commands/arb/index.js +121 -36
- package/dist/commands/arb-auto.d.ts +36 -0
- package/dist/commands/arb-auto.js +202 -76
- package/dist/commands/arb.js +51 -35
- package/dist/commands/bot.d.ts +6 -0
- package/dist/commands/bot.js +55 -45
- package/dist/commands/funds-onchain.d.ts +1 -1
- package/dist/commands/funds-onchain.js +12 -49
- package/dist/commands/funds.js +51 -52
- package/dist/commands/history.js +165 -62
- package/dist/commands/portfolio.d.ts +5 -0
- package/dist/commands/portfolio.js +58 -20
- package/dist/commands/rebalance.js +46 -10
- package/dist/commands/settings.js +2 -1
- package/dist/commands/trade.js +8 -4
- package/dist/commands/wallet.js +74 -155
- package/dist/cross-chain-margin.d.ts +29 -2
- package/dist/cross-chain-margin.js +30 -4
- package/dist/dashboard/server.d.ts +45 -8
- package/dist/dashboard/server.js +113 -42
- package/dist/dashboard/ui.js +46 -27
- package/dist/dashboard/ws-feeds.d.ts +25 -3
- package/dist/dashboard/ws-feeds.js +219 -82
- package/dist/dex-asset-map.d.ts +23 -4
- package/dist/dex-asset-map.js +39 -13
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.d.ts +6 -0
- package/dist/exchanges/arcus/public.js +12 -4
- package/dist/exchanges/aster.d.ts +33 -0
- package/dist/exchanges/aster.js +162 -55
- package/dist/exchanges/hyperliquid.d.ts +30 -0
- package/dist/exchanges/hyperliquid.js +159 -34
- package/dist/exchanges/lighter.d.ts +1 -1
- package/dist/exchanges/lighter.js +11 -9
- package/dist/exchanges/ondo/adapter.d.ts +4 -18
- package/dist/exchanges/ondo/adapter.js +5 -29
- package/dist/exchanges/ondo/funds.d.ts +1 -29
- package/dist/exchanges/ondo/funds.js +2 -94
- package/dist/exchanges/pacifica.d.ts +12 -0
- package/dist/exchanges/pacifica.js +78 -35
- package/dist/exchanges/qfex/adapter.js +4 -1
- package/dist/exchanges/venues/ondo.d.ts +0 -5
- package/dist/exchanges/venues/ondo.js +2 -6
- package/dist/exchanges/venues/qfex.d.ts +11 -0
- package/dist/exchanges/venues/qfex.js +10 -2
- package/dist/mcp-server.js +2 -2
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/rebalance.d.ts +20 -1
- package/dist/rebalance.js +31 -5
- package/dist/strategies/funding-arb.js +38 -7
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +20 -3
- package/skills/perp-cli/references/agent-operations.md +2 -2
- package/skills/perp-cli/references/commands.md +12 -9
|
@@ -30,7 +30,7 @@ import { networkConfig, requireCapabilities } from "../venue.js";
|
|
|
30
30
|
import { parseOrderRef, toLegacyMarketInfo, } from "../venue-types.js";
|
|
31
31
|
import { ArcusClient, arcusLeverageRejectError, arcusRejectionError } from "./client.js";
|
|
32
32
|
import { ed25519PubHexFromBase58 } from "./agent.js";
|
|
33
|
-
import { fetchArcusRawMarkets, toArcusVenueMarket } from "./public.js";
|
|
33
|
+
import { fetchArcusRawMarkets, onlineArcusMarkets, toArcusVenueMarket } from "./public.js";
|
|
34
34
|
import { QUOTE_QUANTUM, assertArcusClientId, canonicalJson, cancelPayload, modifyPayload, nowNs, placePayload, signUtf8Hex, splitEvmSignature, withdrawTypedData, } from "./signing.js";
|
|
35
35
|
/**
|
|
36
36
|
* goodTilTime horizon. Arcus rejects anything nearer than one month at
|
|
@@ -49,6 +49,18 @@ const CLIENT_ID_HISTORY_SCAN = 200;
|
|
|
49
49
|
/** GET /v1/openOrders page cap (default = max = 1000). */
|
|
50
50
|
const OPEN_ORDERS_PAGE = 1000;
|
|
51
51
|
const CANDLE_TIMEFRAMES = new Set(["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w"]);
|
|
52
|
+
/** Bar length (ms) per timeframe — sizes the GET /v1/candles windows. */
|
|
53
|
+
const CANDLE_MS = {
|
|
54
|
+
"1m": 60_000, "3m": 180_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000, "1h": 3_600_000, "2h": 7_200_000,
|
|
55
|
+
"4h": 14_400_000, "8h": 28_800_000, "12h": 43_200_000, "1d": 86_400_000, "3d": 259_200_000, "1w": 604_800_000,
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* GET /v1/candles returns at most 1500 candles (newest first; live 2026-10-13: a 48 h 1m request
|
|
59
|
+
* answers `total: 1500` covering the newest ~25 h). Ranges are read in windows of this many bars.
|
|
60
|
+
*/
|
|
61
|
+
const CANDLE_WINDOW_BARS = 1450;
|
|
62
|
+
/** Window cap per call (≈ 72 500 bars); a wider range is refused rather than truncated. */
|
|
63
|
+
const CANDLE_MAX_WINDOWS = 50;
|
|
52
64
|
// ── Helpers ──
|
|
53
65
|
const STATUS_MAP = {
|
|
54
66
|
ACK: "accepted",
|
|
@@ -447,7 +459,7 @@ export class ArcusAdapter {
|
|
|
447
459
|
// ── TradingV2 ──
|
|
448
460
|
async getVenueMarkets() {
|
|
449
461
|
const raw = await fetchArcusRawMarkets(this.client);
|
|
450
|
-
return raw.map(toArcusVenueMarket);
|
|
462
|
+
return onlineArcusMarkets(raw).map(toArcusVenueMarket);
|
|
451
463
|
}
|
|
452
464
|
async placeOrder(input) {
|
|
453
465
|
// §13.3: GTC is the uniform default for a limit order without a TIF.
|
|
@@ -888,10 +900,25 @@ export class ArcusAdapter {
|
|
|
888
900
|
});
|
|
889
901
|
}
|
|
890
902
|
const m = await this.market(symbol);
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
903
|
+
const span = CANDLE_WINDOW_BARS * CANDLE_MS[interval];
|
|
904
|
+
const windows = Math.max(1, Math.ceil((endTime - startTime) / span));
|
|
905
|
+
if (windows > CANDLE_MAX_WINDOWS) {
|
|
906
|
+
throw new PerpError("INVALID_PARAMS", `Arcus: ${interval} candles over this range need ${windows} requests (max ${CANDLE_MAX_WINDOWS})`, {
|
|
907
|
+
remediation: "Narrow --start / --end or use a longer interval",
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
// Disjoint windows [from, to) — the venue caps one response at 1500 candles, so one wide
|
|
911
|
+
// request would silently drop the oldest bars. Merged and deduped by open time.
|
|
912
|
+
const byTime = new Map();
|
|
913
|
+
for (let from = startTime; from < endTime; from += span) {
|
|
914
|
+
const to = Math.min(from + span, endTime);
|
|
915
|
+
const res = await this.client.get("/v1/candles", "candles", { market: m.marketDisplayName, timeframe: interval, from: from * 1000, to: to * 1000 });
|
|
916
|
+
for (const c of res.candles) {
|
|
917
|
+
const time = usToMs(c.openTime);
|
|
918
|
+
byTime.set(time, { time, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume, trades: c.tradeCount });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return [...byTime.values()].sort((x, y) => x.time - y.time);
|
|
895
922
|
}
|
|
896
923
|
// ── Legacy: account ──
|
|
897
924
|
async rawPositions() {
|
|
@@ -62,4 +62,10 @@ export interface ArcusRawMarket {
|
|
|
62
62
|
export declare function fetchArcusRawMarkets(client: ArcusClient): Promise<ArcusRawMarket[]>;
|
|
63
63
|
export declare function arcusAssetClass(m: ArcusRawMarket): AssetClass;
|
|
64
64
|
export declare function toArcusVenueMarket(m: ArcusRawMarket): VenueMarket;
|
|
65
|
+
/**
|
|
66
|
+
* Markets that are not ONLINE carry placeholder values (live 2026-10-13: the OFFLINE markets
|
|
67
|
+
* F, BAC, CCL, VT, SGOV, RVI report markPrice "0" and fundingRate "0") — they are left out of
|
|
68
|
+
* market / funding lists rather than listed at a $0 price and 0 % funding.
|
|
69
|
+
*/
|
|
70
|
+
export declare function onlineArcusMarkets(raw: ArcusRawMarket[]): ArcusRawMarket[];
|
|
65
71
|
export declare const arcusPublicData: VenuePublicData;
|
|
@@ -76,8 +76,8 @@ function sessionOf(m) {
|
|
|
76
76
|
export function toArcusVenueMarket(m) {
|
|
77
77
|
const assetClass = arcusAssetClass(m);
|
|
78
78
|
const mark = numOrNull(m.markPrice);
|
|
79
|
-
if (mark === null) {
|
|
80
|
-
throw new PerpError("EXCHANGE_ERROR", `Arcus market ${m.marketDisplayName}: missing markPrice`);
|
|
79
|
+
if (mark === null || !(Number(mark) > 0)) {
|
|
80
|
+
throw new PerpError("EXCHANGE_ERROR", `Arcus market ${m.marketDisplayName}: missing / non-positive markPrice (${JSON.stringify(m.markPrice)})`);
|
|
81
81
|
}
|
|
82
82
|
return {
|
|
83
83
|
venue: "arcus",
|
|
@@ -104,15 +104,23 @@ export function toArcusVenueMarket(m) {
|
|
|
104
104
|
tradable: m.status === "ONLINE",
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Markets that are not ONLINE carry placeholder values (live 2026-10-13: the OFFLINE markets
|
|
109
|
+
* F, BAC, CCL, VT, SGOV, RVI report markPrice "0" and fundingRate "0") — they are left out of
|
|
110
|
+
* market / funding lists rather than listed at a $0 price and 0 % funding.
|
|
111
|
+
*/
|
|
112
|
+
export function onlineArcusMarkets(raw) {
|
|
113
|
+
return raw.filter(m => m.status === "ONLINE");
|
|
114
|
+
}
|
|
107
115
|
async function fetchMarkets(net) {
|
|
108
116
|
const raw = await fetchArcusRawMarkets(new ArcusClient(net, null));
|
|
109
|
-
return raw.map(toArcusVenueMarket);
|
|
117
|
+
return onlineArcusMarkets(raw).map(toArcusVenueMarket);
|
|
110
118
|
}
|
|
111
119
|
async function fetchFundingRates(net) {
|
|
112
120
|
const raw = await fetchArcusRawMarkets(new ArcusClient(net, null));
|
|
113
121
|
const out = [];
|
|
114
122
|
const missing = [];
|
|
115
|
-
for (const m of raw) {
|
|
123
|
+
for (const m of onlineArcusMarkets(raw)) {
|
|
116
124
|
const rate = numOrNull(m.fundingRate);
|
|
117
125
|
if (rate === null) {
|
|
118
126
|
missing.push(m.marketDisplayName);
|
|
@@ -61,6 +61,13 @@ type ResolvedSigner = {
|
|
|
61
61
|
/** EVM address of the owner/master — used as `user` field in Aster requests */
|
|
62
62
|
userAddress: string;
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Max GET /fapi/v3/income requests for one 7-day read (7 daily slices + splits of full slices).
|
|
66
|
+
* The endpoint weighs 30 against Aster's 2400/min request weight, so 60 requests = 1800 — inside
|
|
67
|
+
* one minute's budget with room for the per-symbol fan-out. Enough for ~2000 fills/day; beyond
|
|
68
|
+
* that the read refuses (EXCHANGE_ERROR) rather than returning a partial history.
|
|
69
|
+
*/
|
|
70
|
+
export declare const ASTER_INCOME_MAX_PAGES = 60;
|
|
64
71
|
export declare class AsterAdapter implements ExchangeAdapter {
|
|
65
72
|
readonly name = "aster";
|
|
66
73
|
readonly chain = "bnb";
|
|
@@ -207,8 +214,34 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
207
214
|
*/
|
|
208
215
|
invalidateOpenOrdersCache(): void;
|
|
209
216
|
getOpenOrders(): Promise<ExchangeOrder[]>;
|
|
217
|
+
/**
|
|
218
|
+
* Every income row of `incomeType` in the last 7 days. The response order of GET /fapi/v3/income
|
|
219
|
+
* is not documented, so nothing assumes it: the window is read as disjoint [start, end] slices
|
|
220
|
+
* (startTime / endTime inclusive; one day each) and a slice that comes back full (1000 rows, the
|
|
221
|
+
* endpoint max) is split in half and re-read. More than ASTER_INCOME_MAX_PAGES requests, or a
|
|
222
|
+
* full slice of one millisecond → EXCHANGE_ERROR, never a partial list.
|
|
223
|
+
*/
|
|
224
|
+
private _incomeRows;
|
|
225
|
+
/**
|
|
226
|
+
* Symbols with account activity: open positions, open orders, and every symbol with a
|
|
227
|
+
* COMMISSION income row in the last 7 days (see _incomeRows). Aster's userTrades / allOrders
|
|
228
|
+
* both require `symbol`, so this is the per-symbol fan-out set. Orders on a symbol that never
|
|
229
|
+
* filled, is not open and has no position cannot be found (allOrders needs it).
|
|
230
|
+
*/
|
|
231
|
+
private _tradedApiSymbols;
|
|
232
|
+
/**
|
|
233
|
+
* Recent orders across the symbols with account activity (see _tradedApiSymbols), newest first.
|
|
234
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
235
|
+
*/
|
|
210
236
|
getOrderHistory(limit?: number): Promise<ExchangeOrder[]>;
|
|
237
|
+
/**
|
|
238
|
+
* Recent fills. `GET /fapi/v3/userTrades` requires a symbol (asterdex/api-docs V3), so the
|
|
239
|
+
* symbols come from the open positions plus every symbol with a COMMISSION income row in the
|
|
240
|
+
* venue's default window (`GET /fapi/v3/income`: "the recent 7-day data" when no time is sent).
|
|
241
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
242
|
+
*/
|
|
211
243
|
getTradeHistory(limit?: number): Promise<ExchangeTrade[]>;
|
|
244
|
+
/** FUNDING_FEE income of the last 7 days (see _incomeRows), newest first, at most `limit` rows. */
|
|
212
245
|
getFundingPayments(limit?: number): Promise<ExchangeFundingPayment[]>;
|
|
213
246
|
/**
|
|
214
247
|
* Per-market leverage / margin mode: every row of GET /fapi/v3/positionRisk ("Position
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -152,6 +152,13 @@ export function requireAsterClientOrderId(id) {
|
|
|
152
152
|
return id;
|
|
153
153
|
}
|
|
154
154
|
// ── AsterAdapter ──────────────────────────────────────────────────────────────
|
|
155
|
+
/**
|
|
156
|
+
* Max GET /fapi/v3/income requests for one 7-day read (7 daily slices + splits of full slices).
|
|
157
|
+
* The endpoint weighs 30 against Aster's 2400/min request weight, so 60 requests = 1800 — inside
|
|
158
|
+
* one minute's budget with room for the per-symbol fan-out. Enough for ~2000 fills/day; beyond
|
|
159
|
+
* that the read refuses (EXCHANGE_ERROR) rather than returning a partial history.
|
|
160
|
+
*/
|
|
161
|
+
export const ASTER_INCOME_MAX_PAGES = 60;
|
|
155
162
|
export class AsterAdapter {
|
|
156
163
|
name = "aster";
|
|
157
164
|
chain = "bnb";
|
|
@@ -715,79 +722,179 @@ export class AsterAdapter {
|
|
|
715
722
|
this._ordersCache = { data: result, time: Date.now(), epoch: accountCacheEpoch() };
|
|
716
723
|
return result;
|
|
717
724
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
725
|
+
/**
|
|
726
|
+
* Every income row of `incomeType` in the last 7 days. The response order of GET /fapi/v3/income
|
|
727
|
+
* is not documented, so nothing assumes it: the window is read as disjoint [start, end] slices
|
|
728
|
+
* (startTime / endTime inclusive; one day each) and a slice that comes back full (1000 rows, the
|
|
729
|
+
* endpoint max) is split in half and re-read. More than ASTER_INCOME_MAX_PAGES requests, or a
|
|
730
|
+
* full slice of one millisecond → EXCHANGE_ERROR, never a partial list.
|
|
731
|
+
*/
|
|
732
|
+
async _incomeRows(incomeType, r) {
|
|
733
|
+
const end = Date.now();
|
|
734
|
+
const start = end - 7 * 86_400_000;
|
|
735
|
+
const slices = [];
|
|
736
|
+
for (let s = start; s < end; s += 86_400_000)
|
|
737
|
+
slices.push([s, Math.min(s + 86_400_000 - 1, end)]);
|
|
738
|
+
slices[slices.length - 1][1] = end; // last slice ends exactly at `end` (inclusive), no extra 1-ms request
|
|
739
|
+
const rows = [];
|
|
740
|
+
let requests = 0;
|
|
741
|
+
while (slices.length > 0) {
|
|
742
|
+
const [s, e] = slices.shift();
|
|
743
|
+
if (++requests > ASTER_INCOME_MAX_PAGES) {
|
|
744
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster ${incomeType} history needs more than ${ASTER_INCOME_MAX_PAGES} requests for 7 days — the result would be incomplete`, {
|
|
745
|
+
exchange: "aster", remediation: "Check the history per symbol in the Aster web app",
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const page = await this._signedGetEip712("/fapi/v3/income", { incomeType, startTime: String(s), endTime: String(e), limit: "1000" }, r);
|
|
749
|
+
if (!Array.isArray(page)) {
|
|
750
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster income history is not a list (${JSON.stringify(page).slice(0, 120)})`, { exchange: "aster" });
|
|
751
|
+
}
|
|
752
|
+
if (page.length >= 1000) {
|
|
753
|
+
if (e <= s) {
|
|
754
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster returned 1000+ ${incomeType} rows at one millisecond (${s}) — the list cannot be read completely`, {
|
|
755
|
+
exchange: "aster", remediation: "Check the history per symbol in the Aster web app",
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
const mid = Math.floor((s + e) / 2);
|
|
759
|
+
slices.unshift([s, mid], [mid + 1, e]);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
for (const row of page) {
|
|
763
|
+
if (!Number.isFinite(Number(row.time))) {
|
|
764
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster income row without a numeric time: ${JSON.stringify(row).slice(0, 160)}`, { exchange: "aster" });
|
|
765
|
+
}
|
|
766
|
+
rows.push(row);
|
|
724
767
|
}
|
|
725
768
|
}
|
|
769
|
+
return rows;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Symbols with account activity: open positions, open orders, and every symbol with a
|
|
773
|
+
* COMMISSION income row in the last 7 days (see _incomeRows). Aster's userTrades / allOrders
|
|
774
|
+
* both require `symbol`, so this is the per-symbol fan-out set. Orders on a symbol that never
|
|
775
|
+
* filled, is not open and has no position cannot be found (allOrders needs it).
|
|
776
|
+
*/
|
|
777
|
+
async _tradedApiSymbols(r) {
|
|
778
|
+
// Raw venue symbols (no _fromApi → _toApi round trip, which would invent a USDT suffix for a
|
|
779
|
+
// non-USDT market): open positions of GET /fapi/v3/accountWithJoinMargin.
|
|
780
|
+
const account = await this._signedGetEip712("/fapi/v3/accountWithJoinMargin", {}, r);
|
|
781
|
+
if (!Array.isArray(account?.positions)) {
|
|
782
|
+
throw new PerpError("EXCHANGE_ERROR", "Aster accountWithJoinMargin has no positions list", { exchange: "aster" });
|
|
783
|
+
}
|
|
784
|
+
const apiSymbols = new Set();
|
|
785
|
+
for (const p of account.positions) {
|
|
786
|
+
if (typeof p.symbol === "string" && p.symbol !== "" && parseFiniteVenueNumber(p.positionAmt, "position.positionAmt", "aster") !== 0)
|
|
787
|
+
apiSymbols.add(p.symbol);
|
|
788
|
+
}
|
|
789
|
+
const open = await this._signedGetEip712("/fapi/v3/openOrders", {}, r);
|
|
790
|
+
if (!Array.isArray(open)) {
|
|
791
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster open orders is not a list (${JSON.stringify(open).slice(0, 120)})`, { exchange: "aster" });
|
|
792
|
+
}
|
|
793
|
+
for (const o of open) {
|
|
794
|
+
if (typeof o.symbol === "string" && o.symbol !== "")
|
|
795
|
+
apiSymbols.add(o.symbol);
|
|
796
|
+
}
|
|
797
|
+
for (const row of await this._incomeRows("COMMISSION", r)) {
|
|
798
|
+
if (typeof row.symbol === "string" && row.symbol !== "")
|
|
799
|
+
apiSymbols.add(row.symbol);
|
|
800
|
+
}
|
|
801
|
+
return apiSymbols;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Recent orders across the symbols with account activity (see _tradedApiSymbols), newest first.
|
|
805
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
806
|
+
*/
|
|
807
|
+
async getOrderHistory(limit = 30) {
|
|
726
808
|
const r = this._resolveSigner();
|
|
727
|
-
const
|
|
809
|
+
const apiSymbols = await this._tradedApiSymbols(r);
|
|
810
|
+
const field = (o, k) => {
|
|
811
|
+
const v = o[k];
|
|
812
|
+
if ((typeof v === "string" && v !== "") || typeof v === "number")
|
|
813
|
+
return String(v);
|
|
814
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders row without a readable ${k}: ${JSON.stringify(o).slice(0, 160)}`, { exchange: "aster" });
|
|
815
|
+
};
|
|
816
|
+
const rows = [];
|
|
728
817
|
for (const sym of apiSymbols) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
symbol: this._fromApi(String(o.symbol ?? "")),
|
|
738
|
-
side: String(o.side).toLowerCase(),
|
|
739
|
-
price: String(o.price ?? "0"),
|
|
740
|
-
size: String(o.origQty ?? "0"),
|
|
741
|
-
filled: String(o.executedQty ?? "0"),
|
|
742
|
-
status: String(o.status ?? ""),
|
|
743
|
-
type: String(o.type ?? ""),
|
|
744
|
-
triggerPrice: Number(o.stopPrice) > 0 ? String(o.stopPrice) : null,
|
|
745
|
-
});
|
|
818
|
+
const orders = await this._signedGetEip712("/fapi/v3/allOrders", { symbol: sym, limit: String(limit) }, r);
|
|
819
|
+
if (!Array.isArray(orders)) {
|
|
820
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders for ${sym} is not a list (${JSON.stringify(orders).slice(0, 120)})`, { exchange: "aster" });
|
|
821
|
+
}
|
|
822
|
+
for (const o of orders) {
|
|
823
|
+
const side = field(o, "side").toLowerCase();
|
|
824
|
+
if (side !== "buy" && side !== "sell") {
|
|
825
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders row with unknown side "${side}"`, { exchange: "aster" });
|
|
746
826
|
}
|
|
827
|
+
const time = Number(field(o, "time"));
|
|
828
|
+
if (!Number.isFinite(time))
|
|
829
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders row with a non-numeric time`, { exchange: "aster" });
|
|
830
|
+
rows.push({
|
|
831
|
+
time,
|
|
832
|
+
order: {
|
|
833
|
+
orderId: field(o, "orderId"),
|
|
834
|
+
symbol: this._fromApi(field(o, "symbol")),
|
|
835
|
+
side,
|
|
836
|
+
price: field(o, "price"),
|
|
837
|
+
size: field(o, "origQty"),
|
|
838
|
+
filled: field(o, "executedQty"),
|
|
839
|
+
status: field(o, "status"),
|
|
840
|
+
type: field(o, "type"),
|
|
841
|
+
triggerPrice: Number(o.stopPrice) > 0 ? String(o.stopPrice) : null,
|
|
842
|
+
},
|
|
843
|
+
});
|
|
747
844
|
}
|
|
748
|
-
catch { /* skip */ }
|
|
749
845
|
}
|
|
750
|
-
return
|
|
846
|
+
return rows.sort((x, y) => y.time - x.time).slice(0, limit).map(x => x.order);
|
|
751
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Recent fills. `GET /fapi/v3/userTrades` requires a symbol (asterdex/api-docs V3), so the
|
|
850
|
+
* symbols come from the open positions plus every symbol with a COMMISSION income row in the
|
|
851
|
+
* venue's default window (`GET /fapi/v3/income`: "the recent 7-day data" when no time is sent).
|
|
852
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
853
|
+
*/
|
|
752
854
|
async getTradeHistory(limit = 30) {
|
|
753
|
-
const positions = await this.getPositions();
|
|
754
|
-
const apiSymbols = new Set(positions.map(p => this._toApi(p.symbol)));
|
|
755
|
-
if (apiSymbols.size === 0)
|
|
756
|
-
apiSymbols.add("BTCUSDT");
|
|
757
855
|
const r = this._resolveSigner();
|
|
856
|
+
const apiSymbols = await this._tradedApiSymbols(r);
|
|
857
|
+
const field = (t, k) => {
|
|
858
|
+
const v = t[k];
|
|
859
|
+
if ((typeof v === "string" && v !== "") || typeof v === "number")
|
|
860
|
+
return String(v);
|
|
861
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades row without a readable ${k}: ${JSON.stringify(t).slice(0, 160)}`, { exchange: "aster" });
|
|
862
|
+
};
|
|
758
863
|
const allTrades = [];
|
|
759
864
|
for (const sym of apiSymbols) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
time: Number(t.time ?? 0),
|
|
768
|
-
symbol: this._fromApi(String(t.symbol ?? "")),
|
|
769
|
-
side: t.buyer ? "buy" : "sell",
|
|
770
|
-
price: String(t.price ?? "0"),
|
|
771
|
-
size: String(t.qty ?? "0"),
|
|
772
|
-
fee: String(t.commission ?? "0"),
|
|
773
|
-
});
|
|
865
|
+
const trades = await this._signedGetEip712("/fapi/v3/userTrades", { symbol: sym, limit: String(limit) }, r);
|
|
866
|
+
if (!Array.isArray(trades)) {
|
|
867
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades for ${sym} is not a list (${JSON.stringify(trades).slice(0, 120)})`, { exchange: "aster" });
|
|
868
|
+
}
|
|
869
|
+
for (const t of trades) {
|
|
870
|
+
if (typeof t.buyer !== "boolean") {
|
|
871
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades row without a boolean buyer: ${JSON.stringify(t).slice(0, 160)}`, { exchange: "aster" });
|
|
774
872
|
}
|
|
873
|
+
const time = Number(field(t, "time"));
|
|
874
|
+
if (!Number.isFinite(time))
|
|
875
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades row with a non-numeric time`, { exchange: "aster" });
|
|
876
|
+
allTrades.push({
|
|
877
|
+
time,
|
|
878
|
+
symbol: this._fromApi(field(t, "symbol")),
|
|
879
|
+
side: t.buyer ? "buy" : "sell",
|
|
880
|
+
price: field(t, "price"),
|
|
881
|
+
size: field(t, "qty"),
|
|
882
|
+
fee: field(t, "commission"),
|
|
883
|
+
});
|
|
775
884
|
}
|
|
776
|
-
catch { /* skip */ }
|
|
777
885
|
}
|
|
778
886
|
return allTrades.sort((a, b) => b.time - a.time).slice(0, limit);
|
|
779
887
|
}
|
|
888
|
+
/** FUNDING_FEE income of the last 7 days (see _incomeRows), newest first, at most `limit` rows. */
|
|
780
889
|
async getFundingPayments(limit = 200) {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
time:
|
|
788
|
-
|
|
789
|
-
payment: String(f.income ?? "0"),
|
|
790
|
-
}));
|
|
890
|
+
const rows = await this._incomeRows("FUNDING_FEE", this._resolveSigner());
|
|
891
|
+
return rows.map((f) => {
|
|
892
|
+
const time = Number(f.time);
|
|
893
|
+
if (typeof f.symbol !== "string" || f.symbol === "" || (typeof f.income !== "string" && typeof f.income !== "number") || f.income === "") {
|
|
894
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster funding income row without symbol / income: ${JSON.stringify(f).slice(0, 160)}`, { exchange: "aster" });
|
|
895
|
+
}
|
|
896
|
+
return { time, symbol: this._fromApi(f.symbol), payment: String(f.income) };
|
|
897
|
+
}).sort((x, y) => y.time - x.time).slice(0, limit);
|
|
791
898
|
}
|
|
792
899
|
/**
|
|
793
900
|
* Per-market leverage / margin mode: every row of GET /fapi/v3/positionRisk ("Position
|
|
@@ -496,9 +496,39 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
|
|
|
496
496
|
}): Promise<unknown>;
|
|
497
497
|
getRecentTrades(symbol: string, limit?: number): Promise<ExchangeTrade[]>;
|
|
498
498
|
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
499
|
+
/**
|
|
500
|
+
* Order history: POST /info {type:"historicalOrders"} ("Retrieve a user's historical orders",
|
|
501
|
+
* at most 2000 most recent; live 2026-10-13: newest first, each order listed once per status —
|
|
502
|
+
* e.g. "open" then "filled" — with equal statusTimestamp possible). One row per oid with its
|
|
503
|
+
* final status (latest statusTimestamp; on a tie a non-"open" status wins). Includes canceled /
|
|
504
|
+
* rejected orders (the previous userFills read showed fills only). Unreadable rows → EXCHANGE_ERROR.
|
|
505
|
+
* The info endpoint is account-wide; rows are scoped to this adapter (see _inHistoryScope).
|
|
506
|
+
*/
|
|
499
507
|
getOrderHistory(limit?: number): Promise<ExchangeOrder[]>;
|
|
508
|
+
/** Fills: POST /info {type:"userFills"} (at most 2000 most recent, newest first — live 2026-10-13). */
|
|
500
509
|
getTradeHistory(limit?: number): Promise<ExchangeTrade[]>;
|
|
510
|
+
/**
|
|
511
|
+
* Funding payments in this adapter's scope (see _inHistoryScope), newest first, at most `limit`.
|
|
512
|
+
* Reads the last 24 h first and widens to 7 days only when that holds fewer than `limit` rows
|
|
513
|
+
* (POST /info userFunding weighs per row returned; bots poll it).
|
|
514
|
+
*/
|
|
501
515
|
getFundingPayments(limit?: number): Promise<ExchangeFundingPayment[]>;
|
|
516
|
+
/**
|
|
517
|
+
* Every userFunding row since `startTime`. The endpoint returns rows OLDEST first, at most 500
|
|
518
|
+
* per response (live 2026-10-13), and every coin of a funding hour shares one millisecond — so
|
|
519
|
+
* pages continue from the last row's time INCLUSIVE and rows are deduped by time + coin (funding
|
|
520
|
+
* rows carry a zero hash). A full page that adds nothing new, or more than HL_FUNDING_MAX_PAGES
|
|
521
|
+
* pages → EXCHANGE_ERROR (never a list with payments silently missing).
|
|
522
|
+
*/
|
|
523
|
+
private _fundingSince;
|
|
524
|
+
/** The asset map backs _displaySymbol / _inHistoryScope; load it before mapping history rows. */
|
|
525
|
+
private _historyScopeReady;
|
|
526
|
+
/**
|
|
527
|
+
* Whether an account-wide history row (historicalOrders / userFills / userFunding carry every
|
|
528
|
+
* dex, spot "@n" / "PAIR/USDC" and outcome "#n" coins) belongs to this adapter: a HIP-3 dex
|
|
529
|
+
* adapter keeps "<dex>:…" coins, the native adapter keeps native perp coins only.
|
|
530
|
+
*/
|
|
531
|
+
private _inHistoryScope;
|
|
502
532
|
getFundingHistory(symbol: string, limit?: number): Promise<{
|
|
503
533
|
time: number;
|
|
504
534
|
rate: string;
|