perp-cli 0.15.0 → 0.16.0
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/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +129 -535
- package/README.zh.md +243 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +2 -2
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +10 -3
- package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
- package/dist/bot/strategies/funding-arb-utils.js +48 -15
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
- package/dist/bot/strategies/spot-perp-executor.js +2 -2
- package/dist/cli-program.js +1 -1
- package/dist/commands/account.js +168 -24
- package/dist/commands/arb/index.js +19 -77
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +155 -81
- package/dist/commands/arb.d.ts +8 -0
- package/dist/commands/arb.js +49 -23
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +241 -78
- package/dist/commands/options.d.ts +5 -0
- package/dist/commands/options.js +20 -3
- package/dist/commands/pacifica-earn.js +8 -21
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +51 -8
- package/dist/commands/trade.js +4 -4
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +10 -0
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +10 -6
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/aster.d.ts +27 -7
- package/dist/exchanges/aster.js +124 -42
- package/dist/exchanges/canonical-symbols.d.ts +7 -0
- package/dist/exchanges/canonical-symbols.js +20 -0
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/options.d.ts +5 -0
- package/dist/exchanges/derive/options.js +20 -1
- package/dist/exchanges/extended/adapter.d.ts +16 -2
- package/dist/exchanges/extended/adapter.js +96 -34
- package/dist/exchanges/extended/public.d.ts +3 -0
- package/dist/exchanges/extended/public.js +9 -2
- package/dist/exchanges/grvt/adapter.d.ts +16 -2
- package/dist/exchanges/grvt/adapter.js +98 -21
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +180 -13
- package/dist/exchanges/hyperliquid.js +487 -185
- package/dist/exchanges/interface.d.ts +64 -8
- package/dist/exchanges/interface.js +31 -0
- package/dist/exchanges/lighter-public.d.ts +66 -11
- package/dist/exchanges/lighter-public.js +249 -81
- package/dist/exchanges/lighter-spot.d.ts +7 -1
- package/dist/exchanges/lighter-spot.js +99 -90
- package/dist/exchanges/lighter.d.ts +70 -11
- package/dist/exchanges/lighter.js +285 -92
- package/dist/exchanges/ondo/adapter.d.ts +34 -8
- package/dist/exchanges/ondo/adapter.js +73 -16
- package/dist/exchanges/ondo/public.d.ts +27 -2
- package/dist/exchanges/ondo/public.js +63 -31
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-spot.d.ts +6 -0
- package/dist/exchanges/pacifica-spot.js +23 -15
- package/dist/exchanges/pacifica-vaults.d.ts +9 -0
- package/dist/exchanges/pacifica-vaults.js +14 -1
- package/dist/exchanges/pacifica.d.ts +77 -6
- package/dist/exchanges/pacifica.js +321 -130
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +52 -9
- package/dist/exchanges/qfex/adapter.js +74 -9
- package/dist/exchanges/qfex/public.d.ts +62 -9
- package/dist/exchanges/qfex/public.js +128 -22
- package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
- package/dist/exchanges/risex/adapter.d.ts +19 -7
- package/dist/exchanges/risex/adapter.js +58 -12
- package/dist/exchanges/spot-factory.js +1 -1
- package/dist/exchanges/txflow/adapter.d.ts +14 -7
- package/dist/exchanges/txflow/adapter.js +51 -19
- package/dist/exchanges/txflow/client.js +3 -1
- package/dist/exchanges/txflow/public.d.ts +12 -1
- package/dist/exchanges/txflow/public.js +82 -11
- package/dist/exchanges/unknown-symbol.d.ts +6 -1
- package/dist/exchanges/unknown-symbol.js +7 -2
- package/dist/exchanges/variational/adapter.d.ts +2 -6
- package/dist/exchanges/venue-types.d.ts +46 -3
- package/dist/exchanges/venue-types.js +80 -9
- package/dist/exchanges/venue.d.ts +99 -3
- package/dist/exchanges/venue.js +72 -0
- package/dist/exchanges/venues/hyperliquid.js +9 -0
- package/dist/exchanges/venues/lighter-rh.js +13 -3
- package/dist/exchanges/venues/lighter.js +13 -3
- package/dist/exchanges/venues/pacifica.js +9 -0
- package/dist/exchanges/venues/risex.js +15 -0
- package/dist/funding/legacy-public.d.ts +5 -3
- package/dist/funding/legacy-public.js +33 -37
- package/dist/funding/rates.d.ts +54 -5
- package/dist/funding/rates.js +49 -13
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.js +11 -5
- package/dist/mcp-server.js +104 -15
- package/dist/pacifica/client.d.ts +2 -1
- package/dist/pacifica/client.js +11 -0
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/strategies/funding-arb.js +35 -62
- package/dist/utils.d.ts +11 -1
- package/dist/venue-health.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +30 -9
- package/dist/ws/index.js +180 -15
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +30 -9
- package/dist/ws/node.js +204 -16
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/package.json +15 -2
- package/skills/perp-cli/SKILL.md +97 -33
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +15 -12
- package/skills/perp-cli/references/commands.md +67 -12
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +55 -24
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -30,9 +30,23 @@ export const EXTENDED_KLINE_INTERVALS = {
|
|
|
30
30
|
"12h": { wire: "PT12H", ms: 43_200_000 },
|
|
31
31
|
"1d": { wire: "PT24H", ms: 86_400_000 },
|
|
32
32
|
"1w": { wire: "P7D", ms: 604_800_000 },
|
|
33
|
+
// P30D answers calendar-month candles (live 2026-10-19: opens on the 1st, 00:00 UTC); ms is
|
|
34
|
+
// the nominal 30 d used only to size requests.
|
|
35
|
+
"1M": { wire: "P30D", ms: 2_592_000_000 },
|
|
33
36
|
};
|
|
34
|
-
/** Documented per-request maximum of the candles and funding-history endpoints. */
|
|
37
|
+
/** Documented per-request maximum of the candles and funding-history endpoints (accepted `limit`). */
|
|
35
38
|
export const EXTENDED_MAX_RECORDS = 10_000;
|
|
39
|
+
/**
|
|
40
|
+
* Real server caps, measured live 2026-10-19 (BTC-USD; no pagination field, no error): candles
|
|
41
|
+
* return at most 2800 rows on every interval with that much history (PT1M … PT4H: 2800; PT8H 2445,
|
|
42
|
+
* PT12H 1631, PT24H 816, P7D 118, P30D 28 = the whole history since 2024-07), funding history at
|
|
43
|
+
* most 1000 (the newest). Both page backwards with `endTime` = before the oldest row and stop only
|
|
44
|
+
* on an empty page or at the window start — a short page is never taken as the end.
|
|
45
|
+
*/
|
|
46
|
+
export const EXTENDED_CANDLES_PAGE_CAP = 2800;
|
|
47
|
+
export const EXTENDED_FUNDING_PAGE_CAP = 1000;
|
|
48
|
+
const EXTENDED_MAX_PAGES = 50;
|
|
49
|
+
export const EXTENDED_TRADES_CAP = 50;
|
|
36
50
|
const DEC = /^-?\d+(\.\d+)?$/;
|
|
37
51
|
function dec(v, what) {
|
|
38
52
|
if (typeof v !== "string" || !DEC.test(v))
|
|
@@ -92,6 +106,10 @@ export class ExtendedPublicAdapter {
|
|
|
92
106
|
return { bids: side(data.bid, "orderbook bid"), asks: side(data.ask, "orderbook ask") };
|
|
93
107
|
}
|
|
94
108
|
async getRecentTrades(symbol, limit = 50) {
|
|
109
|
+
// The endpoint takes no limit and answers the last 50 trades (live 2026-10-19, `limit=500` → 50).
|
|
110
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > EXTENDED_TRADES_CAP) {
|
|
111
|
+
throw new PerpError("INVALID_PARAMS", `Extended: trades limit must be 1..${EXTENDED_TRADES_CAP} (got ${limit})`, { remediation: `Extended publishes only the last ${EXTENDED_TRADES_CAP} trades` });
|
|
112
|
+
}
|
|
95
113
|
const m = await this.market(symbol);
|
|
96
114
|
const body = await this.client.get(`/info/markets/${encodeURIComponent(m.name)}/trades`);
|
|
97
115
|
if (!Array.isArray(body.data))
|
|
@@ -106,7 +124,10 @@ export class ExtendedPublicAdapter {
|
|
|
106
124
|
// Newest first, like every other venue's tape.
|
|
107
125
|
return trades.sort((a, b) => b.time - a.time).slice(0, limit);
|
|
108
126
|
}
|
|
109
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Applied (settled) hourly rates, newest first — fraction per hour; no oracle price published.
|
|
129
|
+
* Paged backwards in pages of EXTENDED_FUNDING_PAGE_CAP until `limit` rows or the window start.
|
|
130
|
+
*/
|
|
110
131
|
async getFundingHistory(symbol, limit = 10) {
|
|
111
132
|
if (!Number.isInteger(limit) || limit < 1 || limit > EXTENDED_MAX_RECORDS) {
|
|
112
133
|
throw new PerpError("INVALID_PARAMS", `Extended: funding history limit must be 1..${EXTENDED_MAX_RECORDS} (got ${limit})`);
|
|
@@ -115,20 +136,37 @@ export class ExtendedPublicAdapter {
|
|
|
115
136
|
const end = Date.now();
|
|
116
137
|
// One record per hour; two spare hours cover the in-progress hour and clock skew.
|
|
117
138
|
const start = end - (limit + 2) * 3_600_000;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
const byTime = new Map();
|
|
140
|
+
let cursor = end;
|
|
141
|
+
for (let page = 0; byTime.size < limit; page++) {
|
|
142
|
+
if (page >= EXTENDED_MAX_PAGES)
|
|
143
|
+
throw new PerpError("EXCHANGE_ERROR", `Extended: funding history of ${m.name} did not end after ${EXTENDED_MAX_PAGES} pages`);
|
|
144
|
+
const want = Math.min(EXTENDED_FUNDING_PAGE_CAP, limit - byTime.size);
|
|
145
|
+
const body = await this.client.get(`/info/${encodeURIComponent(m.name)}/funding`, [
|
|
146
|
+
["startTime", String(start)], ["endTime", String(cursor)], ["limit", String(want)],
|
|
147
|
+
]);
|
|
148
|
+
if (!Array.isArray(body.data))
|
|
149
|
+
throw extendedShapeError(`funding history of ${m.name} is not a list`);
|
|
150
|
+
const rows = body.data.map(r => {
|
|
151
|
+
const x = r;
|
|
152
|
+
const rate = dec(x.f, "funding f");
|
|
153
|
+
return { time: ms(x.T, "funding T"), rate, price: null, rateKind: "lastSettled", intervalHours: 1, annualizedPct: annualizeHourlyRate(Number(rate)) };
|
|
154
|
+
});
|
|
155
|
+
for (const r of rows)
|
|
156
|
+
byTime.set(r.time, r);
|
|
157
|
+
// Only an empty page ends the window (a short page is not trusted as the end).
|
|
158
|
+
if (rows.length === 0)
|
|
159
|
+
break;
|
|
160
|
+
const oldest = Math.min(...rows.map(r => r.time));
|
|
161
|
+
if (oldest <= start)
|
|
162
|
+
break;
|
|
163
|
+
if (oldest >= cursor)
|
|
164
|
+
throw extendedShapeError(`funding history of ${m.name} did not move back past ${new Date(cursor).toISOString()}`);
|
|
165
|
+
cursor = oldest - 1;
|
|
166
|
+
}
|
|
167
|
+
return [...byTime.values()].sort((a, b) => b.time - a.time).slice(0, limit);
|
|
131
168
|
}
|
|
169
|
+
/** Candles paged backwards from `endTime` in pages of EXTENDED_CANDLES_PAGE_CAP (newest first on the wire). */
|
|
132
170
|
async getKlines(symbol, interval, startTime, endTime) {
|
|
133
171
|
const iv = Object.prototype.hasOwnProperty.call(EXTENDED_KLINE_INTERVALS, interval) ? EXTENDED_KLINE_INTERVALS[interval] : undefined;
|
|
134
172
|
if (iv === undefined) {
|
|
@@ -138,26 +176,50 @@ export class ExtendedPublicAdapter {
|
|
|
138
176
|
}
|
|
139
177
|
if (!(endTime > startTime))
|
|
140
178
|
throw new PerpError("INVALID_PARAMS", "Extended: candle end time must be after the start time");
|
|
179
|
+
const bars = Math.ceil((endTime - startTime) / iv.ms) + 1;
|
|
180
|
+
const pages = Math.ceil(bars / EXTENDED_CANDLES_PAGE_CAP);
|
|
181
|
+
if (pages > EXTENDED_MAX_PAGES) {
|
|
182
|
+
throw new PerpError("INVALID_PARAMS", `Extended: ${interval} candles over this range need ${pages} requests (max ${EXTENDED_MAX_PAGES})`, {
|
|
183
|
+
remediation: "Narrow --start / --end or use a longer interval",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
141
186
|
const m = await this.market(symbol);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
187
|
+
const byTime = new Map();
|
|
188
|
+
// `endTime` is exclusive on the wire (a bar opening exactly at it is not returned): +1 ms makes
|
|
189
|
+
// `--end` inclusive like every other venue.
|
|
190
|
+
let cursor = endTime + 1;
|
|
191
|
+
for (let page = 0; page < EXTENDED_MAX_PAGES + 1; page++) {
|
|
192
|
+
const want = Math.min(EXTENDED_CANDLES_PAGE_CAP, Math.ceil((cursor - startTime) / iv.ms) + 1);
|
|
193
|
+
const body = await this.client.get(`/info/candles/${encodeURIComponent(m.name)}/trades`, [
|
|
194
|
+
["interval", iv.wire], ["limit", String(want)], ["endTime", String(cursor)],
|
|
195
|
+
]);
|
|
196
|
+
if (!Array.isArray(body.data))
|
|
197
|
+
throw extendedShapeError(`candles of ${m.name} is not a list`);
|
|
198
|
+
const rows = body.data.map(c => {
|
|
199
|
+
const x = c;
|
|
200
|
+
return {
|
|
201
|
+
time: ms(x.T, "candle T"),
|
|
202
|
+
open: dec(x.o, "candle o"), high: dec(x.h, "candle h"), low: dec(x.l, "candle l"), close: dec(x.c, "candle c"),
|
|
203
|
+
volume: dec(x.v, "candle v"),
|
|
204
|
+
quoteVolume: null,
|
|
205
|
+
trades: null,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
for (const k of rows)
|
|
209
|
+
if (k.time >= startTime && k.time <= endTime)
|
|
210
|
+
byTime.set(k.time, k);
|
|
211
|
+
// Only an EMPTY page ends the history: a short page is not trusted as the end (the cap is
|
|
212
|
+
// per interval and was measured, not documented), it just costs one more request.
|
|
213
|
+
if (rows.length === 0)
|
|
214
|
+
break;
|
|
215
|
+
const oldest = Math.min(...rows.map(k => k.time));
|
|
216
|
+
if (oldest <= startTime)
|
|
217
|
+
break;
|
|
218
|
+
if (oldest >= cursor)
|
|
219
|
+
throw extendedShapeError(`candles of ${m.name} did not move back past ${new Date(cursor).toISOString()}`);
|
|
220
|
+
cursor = oldest - 1;
|
|
221
|
+
}
|
|
222
|
+
return [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
161
223
|
}
|
|
162
224
|
// ── Account / trading: refused (market data only in this version) ──
|
|
163
225
|
refuse(feature) {
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
* - marketStats.nextFundingRate is (despite its name) the timestamp (ms) of the next application.
|
|
43
43
|
* - rates are published ROUNDED TO 6 DECIMALS (resolution 0.000001 / h ≈ 0.876 % APR): the
|
|
44
44
|
* 0.0000125 baseline reads "0.000013", and a rate below 0.0000005 / h reads "0.000000".
|
|
45
|
+
* There is no more precise public source (checked live 2026-10-19): /info/markets (407
|
|
46
|
+
* markets), /info/markets/<m>/stats and the applied history /info/<m>/funding (`f`, 48 h of
|
|
47
|
+
* ETH / SOL) all carry at most 6 decimals, so the adapter keeps this one read.
|
|
45
48
|
* - `_24_5` RWA equities print 0 while their underlying is closed (TSLA_24_5-USD: 34 of 43
|
|
46
49
|
* weekend prints were 0), so an hourly rate × 8760 overstates them — the venue is
|
|
47
50
|
* fundingVerified false (venues/extended.ts).
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
* - marketStats.nextFundingRate is (despite its name) the timestamp (ms) of the next application.
|
|
43
43
|
* - rates are published ROUNDED TO 6 DECIMALS (resolution 0.000001 / h ≈ 0.876 % APR): the
|
|
44
44
|
* 0.0000125 baseline reads "0.000013", and a rate below 0.0000005 / h reads "0.000000".
|
|
45
|
+
* There is no more precise public source (checked live 2026-10-19): /info/markets (407
|
|
46
|
+
* markets), /info/markets/<m>/stats and the applied history /info/<m>/funding (`f`, 48 h of
|
|
47
|
+
* ETH / SOL) all carry at most 6 decimals, so the adapter keeps this one read.
|
|
45
48
|
* - `_24_5` RWA equities print 0 while their underlying is closed (TSLA_24_5-USD: 34 of 43
|
|
46
49
|
* weekend prints were 0), so an hourly rate × 8760 overstates them — the venue is
|
|
47
50
|
* fundingVerified false (venues/extended.ts).
|
|
@@ -301,7 +304,7 @@ export async function fetchExtendedMarketRows(client, deps = DEFAULT_DEPS) {
|
|
|
301
304
|
return parseExtendedMarkets(body.data, deps.warn);
|
|
302
305
|
}
|
|
303
306
|
export async function fetchExtendedMarkets(net, deps = DEFAULT_DEPS) {
|
|
304
|
-
return (await fetchExtendedMarketRows(extendedClient(net), deps)).map(r => r.market);
|
|
307
|
+
return (await fetchExtendedMarketRows(extendedClient(net), deps)).map(r => ({ ...r.market, status: r.meta.status }));
|
|
305
308
|
}
|
|
306
309
|
/** Funding rows: the current hour's accruing rate (fraction per hour); a market without one has no row. */
|
|
307
310
|
export function toExtendedFundingRates(rows) {
|
|
@@ -418,7 +421,11 @@ export async function resolveExtendedMarketRemote(client, input, deps = DEFAULT_
|
|
|
418
421
|
names = symbols.flatMap(extendedWireCandidates);
|
|
419
422
|
}
|
|
420
423
|
else {
|
|
421
|
-
names
|
|
424
|
+
// Wire names are case-sensitive (`kNOT-USD`): a mixed-case input's own casing first, then the
|
|
425
|
+
// upper-cased guess, then the venue's lower-case "k" (×1000) prefix — before the ~1 MB listing.
|
|
426
|
+
const kPrefixed = /^K[A-Z0-9]/.test(up) ? extendedWireCandidates(up).map(n => `k${n.slice(1)}`) : [];
|
|
427
|
+
const mixedCase = raw !== up && raw !== raw.toLowerCase();
|
|
428
|
+
names = [...new Set([...(mixedCase ? [`${raw}-USD`, `${raw}_24_5-USD`] : []), ...extendedWireCandidates(up), ...kPrefixed])];
|
|
422
429
|
}
|
|
423
430
|
for (const name of names)
|
|
424
431
|
await read(name);
|
|
@@ -12,9 +12,23 @@ import type { Network } from "../venue-types.js";
|
|
|
12
12
|
import { type GrvtVenueMarket } from "./public.js";
|
|
13
13
|
/** perp-cli interval → GRVT CandlestickInterval (api-docs.grvt.io/schemas/candlestick_interval). */
|
|
14
14
|
export declare const GRVT_KLINE_INTERVALS: Readonly<Record<string, string>>;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Bars per kline page (the documented max) and the most pages one read may take. The `next`
|
|
17
|
+
* cursor runs dry after ~1000 bars (live 2026-10-19: page 2 had 2 rows and `next` ""), so pages
|
|
18
|
+
* walk `end_time` back to just before the oldest bar returned instead.
|
|
19
|
+
*/
|
|
16
20
|
export declare const GRVT_KLINE_PAGE = 1000;
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Pages actually read before giving up. Real pages hold fewer than 1000 bars (minutes without a
|
|
23
|
+
* bar: 40 d of 1m took 58 pages for 57,583 bars), so the read budget is 25 % above the nominal
|
|
24
|
+
* GRVT_KLINE_MAX_PAGES. 75 requests stay well inside GRVT's 500 market-data requests / minute.
|
|
25
|
+
*/
|
|
26
|
+
export declare const GRVT_KLINE_READ_PAGES = 75;
|
|
27
|
+
/** Measured live 2026-10-19: /trade answers at most 1000 rows (limit 1500 → 1000, no cursor). */
|
|
28
|
+
export declare const GRVT_TRADES_CAP = 1000;
|
|
29
|
+
/** Measured live 2026-10-19: /funding honours limit 10,000 in one answer (BTC). */
|
|
30
|
+
export declare const GRVT_FUNDING_CAP = 10000;
|
|
31
|
+
export declare const GRVT_KLINE_MAX_PAGES = 60;
|
|
18
32
|
/** Order book depths GRVT serves (api_orderbook_levels_request). */
|
|
19
33
|
export declare const GRVT_BOOK_DEPTHS: readonly [10, 50, 100, 500];
|
|
20
34
|
/**
|
|
@@ -17,9 +17,31 @@ export const GRVT_KLINE_INTERVALS = {
|
|
|
17
17
|
"1h": "CI_1_H", "2h": "CI_2_H", "4h": "CI_4_H", "6h": "CI_6_H", "8h": "CI_8_H", "12h": "CI_12_H",
|
|
18
18
|
"1d": "CI_1_D", "3d": "CI_3_D", "5d": "CI_5_D", "1w": "CI_1_W", "2w": "CI_2_W", "3w": "CI_3_W", "4w": "CI_4_W",
|
|
19
19
|
};
|
|
20
|
-
/**
|
|
20
|
+
/** Fixed-length intervals (ms): `start_time` is floored to their boundary (epoch-aligned, UTC). */
|
|
21
|
+
const GRVT_KLINE_FIXED_MS = {
|
|
22
|
+
"1m": 60_000, "3m": 180_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000, "1h": 3_600_000, "2h": 7_200_000,
|
|
23
|
+
"4h": 14_400_000, "6h": 21_600_000, "8h": 28_800_000, "12h": 43_200_000, "1d": 86_400_000,
|
|
24
|
+
};
|
|
25
|
+
const GRVT_KLINE_NOMINAL_MS = {
|
|
26
|
+
...GRVT_KLINE_FIXED_MS, "3d": 259_200_000, "5d": 432_000_000, "1w": 604_800_000, "2w": 1_209_600_000, "3w": 1_814_400_000, "4w": 2_419_200_000,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Bars per kline page (the documented max) and the most pages one read may take. The `next`
|
|
30
|
+
* cursor runs dry after ~1000 bars (live 2026-10-19: page 2 had 2 rows and `next` ""), so pages
|
|
31
|
+
* walk `end_time` back to just before the oldest bar returned instead.
|
|
32
|
+
*/
|
|
21
33
|
export const GRVT_KLINE_PAGE = 1000;
|
|
22
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Pages actually read before giving up. Real pages hold fewer than 1000 bars (minutes without a
|
|
36
|
+
* bar: 40 d of 1m took 58 pages for 57,583 bars), so the read budget is 25 % above the nominal
|
|
37
|
+
* GRVT_KLINE_MAX_PAGES. 75 requests stay well inside GRVT's 500 market-data requests / minute.
|
|
38
|
+
*/
|
|
39
|
+
export const GRVT_KLINE_READ_PAGES = 75;
|
|
40
|
+
/** Measured live 2026-10-19: /trade answers at most 1000 rows (limit 1500 → 1000, no cursor). */
|
|
41
|
+
export const GRVT_TRADES_CAP = 1000;
|
|
42
|
+
/** Measured live 2026-10-19: /funding honours limit 10,000 in one answer (BTC). */
|
|
43
|
+
export const GRVT_FUNDING_CAP = 10_000;
|
|
44
|
+
export const GRVT_KLINE_MAX_PAGES = 60;
|
|
23
45
|
const WARN = (msg) => { process.stderr.write(`${msg}\n`); };
|
|
24
46
|
/** Order book depths GRVT serves (api_orderbook_levels_request). */
|
|
25
47
|
export const GRVT_BOOK_DEPTHS = [10, 50, 100, 500];
|
|
@@ -91,8 +113,11 @@ export class GrvtPublicAdapter {
|
|
|
91
113
|
}
|
|
92
114
|
async getOrderbook(symbol, depth = 50) {
|
|
93
115
|
const m = await this.market(symbol);
|
|
94
|
-
// The smallest served depth that covers the request; above 500
|
|
95
|
-
const d = GRVT_BOOK_DEPTHS.find(x => x >= depth)
|
|
116
|
+
// The smallest served depth that covers the request; above 500 → INVALID_PARAMS (never clamped).
|
|
117
|
+
const d = GRVT_BOOK_DEPTHS.find(x => x >= depth);
|
|
118
|
+
if (d === undefined || !Number.isInteger(depth) || depth < 1) {
|
|
119
|
+
throw new PerpError("INVALID_PARAMS", `GRVT: book depth must be 1..500 (got ${depth})`, { exchange: "grvt", remediation: "Pass --depth 1..500" });
|
|
120
|
+
}
|
|
96
121
|
const res = await this.client.result("book", { instrument: m.instrument, depth: d });
|
|
97
122
|
if (res === null || typeof res !== "object" || !Array.isArray(res.bids) || !Array.isArray(res.asks)) {
|
|
98
123
|
throw grvtShapeError("book has no bids / asks arrays");
|
|
@@ -105,10 +130,16 @@ export class GrvtPublicAdapter {
|
|
|
105
130
|
return { bids: side(res.bids.slice(0, depth), "bid"), asks: side(res.asks.slice(0, depth), "ask") };
|
|
106
131
|
}
|
|
107
132
|
async getRecentTrades(symbol, limit = 50) {
|
|
133
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > GRVT_TRADES_CAP) {
|
|
134
|
+
throw new PerpError("INVALID_PARAMS", `GRVT: trades limit must be 1..${GRVT_TRADES_CAP} (got ${limit})`, {
|
|
135
|
+
remediation: `GRVT serves at most ${GRVT_TRADES_CAP} recent trades per request and no cursor for older ones`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
108
138
|
const m = await this.market(symbol);
|
|
109
|
-
const res = await this.client.result("trade", { instrument: m.instrument, limit
|
|
139
|
+
const res = await this.client.result("trade", { instrument: m.instrument, limit });
|
|
110
140
|
if (!Array.isArray(res))
|
|
111
141
|
throw grvtShapeError("trade result is not an array");
|
|
142
|
+
// The venue answers oldest first; every venue's tape is newest first.
|
|
112
143
|
return res.map(t => {
|
|
113
144
|
const x = t;
|
|
114
145
|
if (typeof x.is_taker_buyer !== "boolean")
|
|
@@ -121,15 +152,19 @@ export class GrvtPublicAdapter {
|
|
|
121
152
|
size: dec(x.size, "trade size"),
|
|
122
153
|
fee: null,
|
|
123
154
|
};
|
|
124
|
-
});
|
|
155
|
+
}).sort((a, b) => b.time - a.time);
|
|
125
156
|
}
|
|
126
157
|
/**
|
|
127
158
|
* Settled funding (/full/v1/funding, one record per funding interval, newest first). `rate` is a
|
|
128
159
|
* fraction per the record's `intervalHours` (published in percentage points, converted exactly).
|
|
129
160
|
*/
|
|
130
161
|
async getFundingHistory(symbol, limit = 100) {
|
|
162
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > GRVT_FUNDING_CAP) {
|
|
163
|
+
throw new PerpError("INVALID_PARAMS", `GRVT: funding history limit must be 1..${GRVT_FUNDING_CAP} (got ${limit})`);
|
|
164
|
+
}
|
|
131
165
|
const m = await this.market(symbol);
|
|
132
|
-
|
|
166
|
+
// Sent as asked (it was silently clamped to 1000; the venue serves 10,000 in one answer).
|
|
167
|
+
const res = await this.client.result("funding", { instrument: m.instrument, limit });
|
|
133
168
|
if (!Array.isArray(res))
|
|
134
169
|
throw grvtShapeError("funding result is not an array");
|
|
135
170
|
return res.map(r => {
|
|
@@ -154,28 +189,63 @@ export class GrvtPublicAdapter {
|
|
|
154
189
|
remediation: `Use one of: ${Object.keys(GRVT_KLINE_INTERVALS).join(", ")}`,
|
|
155
190
|
});
|
|
156
191
|
}
|
|
192
|
+
if (!(endTime > startTime))
|
|
193
|
+
throw new PerpError("INVALID_PARAMS", "GRVT: candle end time must be after the start time");
|
|
194
|
+
const fixed = GRVT_KLINE_FIXED_MS[interval];
|
|
195
|
+
// An off-grid start_time makes GRVT answer a truncated first bar labelled off the grid
|
|
196
|
+
// (live: 4h from 05:07 → a 06:00–08:00 bar), so the start is floored to the boundary.
|
|
197
|
+
const start = fixed === undefined ? startTime : Math.floor(startTime / fixed) * fixed;
|
|
198
|
+
const pagesNeeded = Math.ceil((endTime - start) / GRVT_KLINE_NOMINAL_MS[interval] / GRVT_KLINE_PAGE);
|
|
199
|
+
if (pagesNeeded > GRVT_KLINE_MAX_PAGES) {
|
|
200
|
+
throw new PerpError("INVALID_PARAMS", `GRVT: ${interval} candles over this range need ${pagesNeeded} requests (max ${GRVT_KLINE_MAX_PAGES})`, {
|
|
201
|
+
remediation: "Narrow --start / --end or use a longer interval",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
157
204
|
const m = await this.market(symbol);
|
|
158
|
-
// Reverse-chronological pages of up to 1000 bars
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
205
|
+
// Reverse-chronological pages of up to 1000 bars; the next page ends just before the oldest
|
|
206
|
+
// bar returned. Done when a page is empty or reaches `start` — never on the cursor alone.
|
|
207
|
+
const byOpen = new Map();
|
|
208
|
+
let end = endTime;
|
|
209
|
+
let oldestSeen = Infinity;
|
|
210
|
+
let exhausted = false;
|
|
211
|
+
for (let page = 0; page < GRVT_KLINE_READ_PAGES; page++) {
|
|
162
212
|
const res = await this.client.post("kline", {
|
|
163
|
-
instrument: m.instrument, interval: ci, type: "TRADE", start_time: msToNs(
|
|
164
|
-
...(cursor !== "" ? { cursor } : {}),
|
|
213
|
+
instrument: m.instrument, interval: ci, type: "TRADE", start_time: msToNs(start), end_time: msToNs(end), limit: GRVT_KLINE_PAGE,
|
|
165
214
|
});
|
|
166
215
|
if (res === null || typeof res !== "object" || !Array.isArray(res.result))
|
|
167
216
|
throw grvtShapeError("kline result is not an array");
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
217
|
+
// A fixed-interval bar whose first minute had no trade is labelled at its first trade (live
|
|
218
|
+
// 2026-10-19: the 09:30–10:00 30m bar opened "09:31"); its open is the bucket boundary, and a
|
|
219
|
+
// page asked to end before that label answers the same bar again — it is not an older bar.
|
|
220
|
+
const inPage = res.result
|
|
221
|
+
.map(c => ({ c, t: fixed === undefined ? nsToMs(c.open_time, "kline open_time") : Math.floor(nsToMs(c.open_time, "kline open_time") / fixed) * fixed }))
|
|
222
|
+
.filter(x => page === 0 || x.t < oldestSeen);
|
|
223
|
+
// No bar older than the last page (an empty page, or only bars already read): history starts here.
|
|
224
|
+
if (inPage.length === 0) {
|
|
225
|
+
exhausted = true;
|
|
174
226
|
break;
|
|
175
227
|
}
|
|
176
|
-
|
|
228
|
+
let oldest = Infinity;
|
|
229
|
+
for (const { c, t } of inPage) {
|
|
230
|
+
byOpen.set(String(t), { ...c, open_time: `${t}000000` });
|
|
231
|
+
if (t < oldest)
|
|
232
|
+
oldest = t;
|
|
233
|
+
}
|
|
234
|
+
oldestSeen = oldest;
|
|
235
|
+
if (oldest <= start)
|
|
236
|
+
break;
|
|
237
|
+
end = oldest - 1;
|
|
238
|
+
}
|
|
239
|
+
// Ran out of the read budget before reaching `start`: the oldest bars would be missing — never
|
|
240
|
+
// returned as the whole range.
|
|
241
|
+
if (!exhausted && oldestSeen > start) {
|
|
242
|
+
throw new PerpError("EXCHANGE_ERROR", `GRVT: ${interval} candles reached only ${new Date(oldestSeen).toISOString()} after ${GRVT_KLINE_READ_PAGES} pages (requested from ${new Date(start).toISOString()})`, {
|
|
243
|
+
exchange: "grvt", reachedMs: oldestSeen, requestedFromMs: start,
|
|
244
|
+
remediation: `Narrow --start / --end (or use a longer interval); fetch older bars with --end ${oldestSeen - 1}`,
|
|
245
|
+
});
|
|
177
246
|
}
|
|
178
|
-
|
|
247
|
+
const rows = [...byOpen.values()];
|
|
248
|
+
const bars = rows.map(c => {
|
|
179
249
|
const x = c;
|
|
180
250
|
return {
|
|
181
251
|
time: nsToMs(x.open_time, "kline open_time"),
|
|
@@ -183,8 +253,15 @@ export class GrvtPublicAdapter {
|
|
|
183
253
|
volume: dec(x.volume_b, "kline volume_b"),
|
|
184
254
|
quoteVolume: typeof x.volume_q === "string" && DEC.test(x.volume_q) ? x.volume_q : null,
|
|
185
255
|
trades: typeof x.trades === "number" ? x.trades : null,
|
|
256
|
+
closeMs: nsToMs(x.close_time, "kline close_time"),
|
|
186
257
|
};
|
|
187
258
|
}).sort((a, b) => a.time - b.time);
|
|
259
|
+
// Calendar intervals (3d … 4w) are not floored: a first bar shorter than the interval is a
|
|
260
|
+
// truncated bar (live: the first 1w bar covered 4 days) and is left out. The newest bar may be
|
|
261
|
+
// in progress and is kept.
|
|
262
|
+
const nominal = GRVT_KLINE_NOMINAL_MS[interval];
|
|
263
|
+
const full = fixed === undefined && bars.length > 1 && bars[0].closeMs - bars[0].time + 1 < nominal * 0.99 ? bars.slice(1) : bars;
|
|
264
|
+
return full.map(({ closeMs: _c, ...k }) => k);
|
|
188
265
|
}
|
|
189
266
|
// ── Account / trading: refused (read-only in this version) ──
|
|
190
267
|
refuse(feature) {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { OutcomeAdapter, OutcomeMarketInfo, OutcomeOrderbook, OutcomePosition, OutcomeView, OutcomeViewUnderlying } from "./outcome-interface.js";
|
|
17
17
|
import type { HyperliquidAdapter } from "./hyperliquid.js";
|
|
18
|
+
/** Bid–ask width (probability points) above which a side's mid-implied probability is flagged. */
|
|
19
|
+
export declare const OUTCOME_WIDE_SPREAD = 0.2;
|
|
18
20
|
export declare class HyperliquidOutcomeAdapter implements OutcomeAdapter {
|
|
19
21
|
readonly name = "hyperliquid";
|
|
20
22
|
private _hl;
|
|
@@ -22,6 +22,16 @@ const MIN_ORDER_QUOTE = 10;
|
|
|
22
22
|
const MAX_SIDE = 9;
|
|
23
23
|
/** Asset id offset spans `[100_000_000, 200_000_000)`, so encoding < 100M. */
|
|
24
24
|
const MAX_ENCODING = 99_999_999;
|
|
25
|
+
/** A numeric description field; a malformed one is left unset (unknown) with a warning, never NaN. */
|
|
26
|
+
function finiteOrWarn(val, key, description) {
|
|
27
|
+
const n = val.trim() === "" ? NaN : Number(val);
|
|
28
|
+
if (Number.isFinite(n))
|
|
29
|
+
return n;
|
|
30
|
+
process.stderr.write(`[hyperliquid] outcome description ${key} "${val}" is not a number — target price unknown (${description.slice(0, 120)})\n`);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Bid–ask width (probability points) above which a side's mid-implied probability is flagged. */
|
|
34
|
+
export const OUTCOME_WIDE_SPREAD = 0.2;
|
|
25
35
|
export class HyperliquidOutcomeAdapter {
|
|
26
36
|
name = "hyperliquid";
|
|
27
37
|
_hl;
|
|
@@ -76,9 +86,17 @@ export class HyperliquidOutcomeAdapter {
|
|
|
76
86
|
else if (key === "expiry")
|
|
77
87
|
out.expiryMs = HyperliquidOutcomeAdapter._parseExpiry(val);
|
|
78
88
|
else if (key === "targetPrice")
|
|
79
|
-
out.targetPrice =
|
|
89
|
+
out.targetPrice = finiteOrWarn(val, key, description);
|
|
80
90
|
else if (key === "period")
|
|
81
91
|
out.period = val;
|
|
92
|
+
// Template outcomes (name "template:binaryPrice", live 2026-10-19):
|
|
93
|
+
// "perp:ZEC|priceDescription:…|seconds:90|threshold:1057|time:20261023-0800".
|
|
94
|
+
else if (key === "perp" && out.underlying === undefined)
|
|
95
|
+
out.underlying = val;
|
|
96
|
+
else if (key === "threshold" && out.targetPrice === undefined)
|
|
97
|
+
out.targetPrice = finiteOrWarn(val, key, description);
|
|
98
|
+
else if (key === "time" && out.expiryMs === undefined)
|
|
99
|
+
out.expiryMs = HyperliquidOutcomeAdapter._parseExpiry(val);
|
|
82
100
|
}
|
|
83
101
|
return out;
|
|
84
102
|
}
|
|
@@ -315,6 +333,9 @@ export class HyperliquidOutcomeAdapter {
|
|
|
315
333
|
});
|
|
316
334
|
}
|
|
317
335
|
const parsed = HyperliquidOutcomeAdapter.parseDescription(meta.description);
|
|
336
|
+
// A template binary-price outcome is a price binary (Yes = underlying ≥ threshold at `time`).
|
|
337
|
+
if (parsed.class === undefined && meta.name === "template:binaryPrice")
|
|
338
|
+
parsed.class = "priceBinary";
|
|
318
339
|
// Fetch books for every side in parallel + allMids once for mids and
|
|
319
340
|
// the underlying symbol's mark price.
|
|
320
341
|
const allMidsPromise = this._infoPost({ type: "allMids" });
|
|
@@ -336,6 +357,13 @@ export class HyperliquidOutcomeAdapter {
|
|
|
336
357
|
bestBid: trimmed.bestBid,
|
|
337
358
|
bestAsk: trimmed.bestAsk,
|
|
338
359
|
impliedProb: mid !== undefined ? Number(mid) : undefined,
|
|
360
|
+
// Template side names ("template:Yes") without the prefix; `name` stays as published.
|
|
361
|
+
label: spec.name.replace(/^template:/, ""),
|
|
362
|
+
// The mid of a book wider than OUTCOME_WIDE_SPREAD (e.g. 0.0257 / 0.9899) says little
|
|
363
|
+
// about the probability: flagged, never dropped.
|
|
364
|
+
wideSpread: trimmed.bestBid !== undefined && trimmed.bestAsk !== undefined
|
|
365
|
+
? Number(trimmed.bestAsk) - Number(trimmed.bestBid) > OUTCOME_WIDE_SPREAD
|
|
366
|
+
: null,
|
|
339
367
|
};
|
|
340
368
|
});
|
|
341
369
|
const midSum = HyperliquidOutcomeAdapter._computeMidSum(sides);
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
import type { SpotAdapter, SpotMarketInfo, SpotBalance, SpotHoldingPrice, SpotHoldingPricesCapable } from "./spot-interface.js";
|
|
10
10
|
import type { ResolvedSlippage } from "./slippage.js";
|
|
11
11
|
import type { HyperliquidAdapter } from "./hyperliquid.js";
|
|
12
|
+
import { type HlAbstractionMode } from "./hyperliquid.js";
|
|
13
|
+
/**
|
|
14
|
+
* Spot balances from info `spotClearinghouseState` — the one mapper behind the keyed spot adapter
|
|
15
|
+
* and the keyless `account balance --address`. `shared` (already counted in the perp equity) follows
|
|
16
|
+
* the account mode: unified → the USDC row; portfolio margin → USDC and every eligible collateral row
|
|
17
|
+
* (the venue marks those with `ltv`); any other mode, or `null` (a HIP-3 dex scope / mode unknown) → none.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hlSpotBalanceRows(state: Record<string, unknown>, mode: HlAbstractionMode | null): SpotBalance[];
|
|
12
20
|
export declare class HyperliquidSpotAdapter implements SpotAdapter, SpotHoldingPricesCapable {
|
|
13
21
|
readonly name = "hyperliquid";
|
|
14
22
|
private _hl;
|
|
@@ -16,6 +16,24 @@ import { invalidateCache } from "../cache.js";
|
|
|
16
16
|
function hlRoundSize(size, szDecimals) {
|
|
17
17
|
return floorSize(size, hlSizeStep(szDecimals));
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Spot balances from info `spotClearinghouseState` — the one mapper behind the keyed spot adapter
|
|
21
|
+
* and the keyless `account balance --address`. `shared` (already counted in the perp equity) follows
|
|
22
|
+
* the account mode: unified → the USDC row; portfolio margin → USDC and every eligible collateral row
|
|
23
|
+
* (the venue marks those with `ltv`); any other mode, or `null` (a HIP-3 dex scope / mode unknown) → none.
|
|
24
|
+
*/
|
|
25
|
+
export function hlSpotBalanceRows(state, mode) {
|
|
26
|
+
const balances = (state?.balances ?? []);
|
|
27
|
+
return balances.map((b) => ({
|
|
28
|
+
token: String(b.coin ?? ""),
|
|
29
|
+
total: String(b.total ?? "0"),
|
|
30
|
+
available: String(Number(b.total ?? 0) - Number(b.hold ?? 0)),
|
|
31
|
+
held: String(b.hold ?? "0"),
|
|
32
|
+
entryNtl: b.entryNtl !== undefined ? String(b.entryNtl) : undefined,
|
|
33
|
+
shared: (mode === "unified" || mode === "portfolio") && String(b.coin ?? "") === "USDC"
|
|
34
|
+
|| mode === "portfolio" && Object.prototype.hasOwnProperty.call(b, "ltv"),
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
19
37
|
export class HyperliquidSpotAdapter {
|
|
20
38
|
name = "hyperliquid";
|
|
21
39
|
_hl;
|
|
@@ -181,18 +199,8 @@ export class HyperliquidSpotAdapter {
|
|
|
181
199
|
// in the funding-arb bot (and a misleading empty holdings table).
|
|
182
200
|
// Reuse cached spot clearinghouse state (shared with getBalance() — saves 1 API call).
|
|
183
201
|
const state = await this._hl._getSpotClearinghouseState({ fresh });
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// HIP-3 dex-scoped adapters report dex equity, which never contains spot USDC.
|
|
187
|
-
const unified = this._hl.isUnifiedAccount === true && this._hl.dex === "";
|
|
188
|
-
return balances.map((b) => ({
|
|
189
|
-
token: String(b.coin ?? ""),
|
|
190
|
-
total: String(b.total ?? "0"),
|
|
191
|
-
available: String(Number(b.total ?? 0) - Number(b.hold ?? 0)),
|
|
192
|
-
held: String(b.hold ?? "0"),
|
|
193
|
-
entryNtl: b.entryNtl !== undefined ? String(b.entryNtl) : undefined,
|
|
194
|
-
shared: unified && String(b.coin ?? "") === "USDC",
|
|
195
|
-
}));
|
|
202
|
+
// HIP-3 dex-scoped adapters report dex equity, which never contains spot balances.
|
|
203
|
+
return hlSpotBalanceRows(state, this._hl.dex === "" ? this._hl.accountMode : null);
|
|
196
204
|
}
|
|
197
205
|
async spotMarketOrder(symbol, side, size, opts) {
|
|
198
206
|
const what = `spot ${side} ${size} ${symbol}`;
|