@pear-protocol/exchanges-sdk 0.12.2 → 0.14.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.md +1 -0
- package/dist/client/hyperliquid/rest.js +12 -2
- package/dist/client/lighter/rest.js +44 -12
- package/dist/index.d.ts +94 -21
- package/dist/sdk.js +7 -1
- package/dist/syncer/base.js +2 -0
- package/dist/syncer/hyperliquid/syncer.js +38 -14
- package/dist/syncer/lighter/syncer.js +3 -3
- package/dist/tracker/binance/mapper.js +2 -0
- package/dist/tracker/binance/tracker.js +2 -0
- package/dist/tracker/bybit/mapper.js +2 -0
- package/dist/tracker/hyperliquid/balance.js +16 -0
- package/dist/tracker/hyperliquid/mapper.js +11 -0
- package/dist/tracker/lighter/balance.js +16 -0
- package/dist/tracker/lighter/mapper.js +21 -0
- package/dist/tracker/lighter/tracker.js +65 -38
- package/dist/tracker/okx/mapper.js +2 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -291,6 +291,7 @@ tracker.trackPosition((positions) => {
|
|
|
291
291
|
p.size;
|
|
292
292
|
p.entryPrice;
|
|
293
293
|
p.unrealizedPnl;
|
|
294
|
+
p.marginUsed; // string | null — null where the venue publishes none
|
|
294
295
|
p.leverage;
|
|
295
296
|
p.marginType; // "cross" | "isolated"
|
|
296
297
|
p.liquidationPrice; // string | null
|
|
@@ -6,6 +6,16 @@ const HL_INFO_URLS = {
|
|
|
6
6
|
mainnet: "https://api.hyperliquid.xyz/info",
|
|
7
7
|
testnet: "https://api.hyperliquid-testnet.xyz/info"
|
|
8
8
|
};
|
|
9
|
+
class HyperliquidInfoRequestError extends Error {
|
|
10
|
+
static {
|
|
11
|
+
__name(this, "HyperliquidInfoRequestError");
|
|
12
|
+
}
|
|
13
|
+
status;
|
|
14
|
+
constructor(status) {
|
|
15
|
+
super(`Hyperliquid info request failed: ${status}`);
|
|
16
|
+
this.status = status;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
async function fetchInfo(body, demo = false) {
|
|
10
20
|
const url = demo ? HL_INFO_URLS.testnet : HL_INFO_URLS.mainnet;
|
|
11
21
|
const res = await proxyFetch(url, {
|
|
@@ -16,7 +26,7 @@ async function fetchInfo(body, demo = false) {
|
|
|
16
26
|
body: JSON.stringify(body)
|
|
17
27
|
});
|
|
18
28
|
if (!res.ok) {
|
|
19
|
-
throw new
|
|
29
|
+
throw new HyperliquidInfoRequestError(res.status);
|
|
20
30
|
}
|
|
21
31
|
return res.json();
|
|
22
32
|
}
|
|
@@ -85,4 +95,4 @@ async function fetchActiveAssetData(user, coin, demo = false) {
|
|
|
85
95
|
}
|
|
86
96
|
__name(fetchActiveAssetData, "fetchActiveAssetData");
|
|
87
97
|
|
|
88
|
-
export { fetchActiveAssetData, fetchAllMids, fetchClearinghouseState, fetchDelegatorSummary, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction, fetchUserRole, fetchUserVaultEquities };
|
|
98
|
+
export { HyperliquidInfoRequestError, fetchActiveAssetData, fetchAllMids, fetchClearinghouseState, fetchDelegatorSummary, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction, fetchUserRole, fetchUserVaultEquities };
|
|
@@ -3,6 +3,17 @@ import { getLighterBaseUrl } from './constants';
|
|
|
3
3
|
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
+
class LighterRequestError extends Error {
|
|
7
|
+
static {
|
|
8
|
+
__name(this, "LighterRequestError");
|
|
9
|
+
}
|
|
10
|
+
status;
|
|
11
|
+
constructor(message, status) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "LighterRequestError";
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
const INTENT_REQUEST_ATTEMPTS = 3;
|
|
7
18
|
const INTENT_RETRY_BASE_DELAY_MS = 500;
|
|
8
19
|
const RETRYABLE_INTENT_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -176,9 +187,11 @@ async function fetchAccountsByL1Address(l1Address, demo = false) {
|
|
|
176
187
|
};
|
|
177
188
|
}
|
|
178
189
|
__name(fetchAccountsByL1Address, "fetchAccountsByL1Address");
|
|
179
|
-
async function fetchAccount(accountIndex, authToken, demo = false) {
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
async function fetchAccount(accountIndex, authToken, demo = false, throttle) {
|
|
191
|
+
return gatedLighterRead(throttle, "account", async () => {
|
|
192
|
+
const response = await authenticatedLighterFetch(`${getLighterBaseUrl(demo)}/api/v1/account?by=index&value=${accountIndex}`, authToken);
|
|
193
|
+
return readLighterResponse(response, "/account");
|
|
194
|
+
}, () => 0);
|
|
182
195
|
}
|
|
183
196
|
__name(fetchAccount, "fetchAccount");
|
|
184
197
|
async function fetchPublicAccount(accountIndex, demo = false) {
|
|
@@ -232,7 +245,7 @@ async function fetchAssetDetails(demo) {
|
|
|
232
245
|
return data.asset_details;
|
|
233
246
|
}
|
|
234
247
|
__name(fetchAssetDetails, "fetchAssetDetails");
|
|
235
|
-
async function fetchTradesSince(accountIndex, authToken, since, demo = false) {
|
|
248
|
+
async function fetchTradesSince(accountIndex, authToken, since, demo = false, throttle) {
|
|
236
249
|
const results = [];
|
|
237
250
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
238
251
|
let cursor;
|
|
@@ -251,8 +264,10 @@ async function fetchTradesSince(accountIndex, authToken, since, demo = false) {
|
|
|
251
264
|
cursor
|
|
252
265
|
} : {}
|
|
253
266
|
});
|
|
254
|
-
const
|
|
255
|
-
|
|
267
|
+
const data = await gatedLighterRead(throttle, "trades", async () => {
|
|
268
|
+
const response = await authenticatedLighterFetch(`${getLighterBaseUrl(demo)}/api/v1/trades?${params.toString()}`, authToken);
|
|
269
|
+
return readLighterResponse(response, "/trades");
|
|
270
|
+
}, (page2) => (page2.trades ?? []).length);
|
|
256
271
|
const page = data.trades ?? [];
|
|
257
272
|
results.push(...page.filter((trade) => trade.timestamp >= since.getTime()));
|
|
258
273
|
const reachedSince = page.some((trade) => trade.timestamp < since.getTime());
|
|
@@ -269,7 +284,7 @@ async function fetchTradesSince(accountIndex, authToken, since, demo = false) {
|
|
|
269
284
|
return Array.from(byId.values());
|
|
270
285
|
}
|
|
271
286
|
__name(fetchTradesSince, "fetchTradesSince");
|
|
272
|
-
async function fetchPositionFundingSince(accountIndex, authToken, since, demo = false) {
|
|
287
|
+
async function fetchPositionFundingSince(accountIndex, authToken, since, demo = false, throttle) {
|
|
273
288
|
const results = /* @__PURE__ */ new Map();
|
|
274
289
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
275
290
|
let cursor;
|
|
@@ -284,8 +299,10 @@ async function fetchPositionFundingSince(accountIndex, authToken, since, demo =
|
|
|
284
299
|
cursor
|
|
285
300
|
} : {}
|
|
286
301
|
});
|
|
287
|
-
const
|
|
288
|
-
|
|
302
|
+
const data = await gatedLighterRead(throttle, "positionFunding", async () => {
|
|
303
|
+
const response = await authenticatedLighterFetch(`${getLighterBaseUrl(demo)}/api/v1/positionFunding?${params.toString()}`, authToken);
|
|
304
|
+
return readLighterResponse(response, "/positionFunding");
|
|
305
|
+
}, (page) => (page.position_fundings ?? []).length);
|
|
289
306
|
for (const funding of data.position_fundings ?? []) {
|
|
290
307
|
if (funding.timestamp >= sinceSeconds) {
|
|
291
308
|
results.set(`${funding.market_id}:${funding.funding_id}`, funding);
|
|
@@ -374,6 +391,21 @@ async function fetchUserReferrals(params, demo = false) {
|
|
|
374
391
|
};
|
|
375
392
|
}
|
|
376
393
|
__name(fetchUserReferrals, "fetchUserReferrals");
|
|
394
|
+
async function gatedLighterRead(throttle, requestType, run, countItems) {
|
|
395
|
+
if (!throttle) {
|
|
396
|
+
return run();
|
|
397
|
+
}
|
|
398
|
+
await throttle.beforeRequest(requestType);
|
|
399
|
+
try {
|
|
400
|
+
const result = await run();
|
|
401
|
+
throttle.afterRequest(requestType, countItems(result));
|
|
402
|
+
return result;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
throttle.afterRequestError(requestType, error);
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
__name(gatedLighterRead, "gatedLighterRead");
|
|
377
409
|
async function authenticatedLighterFetch(url, authToken, init) {
|
|
378
410
|
const headers = new Headers(init?.headers);
|
|
379
411
|
headers.set("authorization", authToken);
|
|
@@ -440,10 +472,10 @@ async function readLighterResponse(response, endpoint, acceptedCodes = [
|
|
|
440
472
|
data = void 0;
|
|
441
473
|
}
|
|
442
474
|
if (!data) {
|
|
443
|
-
throw new
|
|
475
|
+
throw new LighterRequestError(`Lighter ${endpoint} returned ${response.status}`, response.status);
|
|
444
476
|
}
|
|
445
477
|
if (!acceptedCodes.includes(data.code)) {
|
|
446
|
-
throw new
|
|
478
|
+
throw new LighterRequestError(`Lighter ${endpoint} returned ${data.code}: ${data.message ?? `HTTP ${response.status}`}`, response.status);
|
|
447
479
|
}
|
|
448
480
|
return data;
|
|
449
481
|
}
|
|
@@ -493,4 +525,4 @@ function readReferralAmount(value) {
|
|
|
493
525
|
}
|
|
494
526
|
__name(readReferralAmount, "readReferralAmount");
|
|
495
527
|
|
|
496
|
-
export { createCctpDepositIntentAddress, fetchAccount, fetchAccountLimits, fetchAccountMetadata, fetchAccountsByL1Address, fetchApiKeys, fetchAssetDetails, fetchDepositHistory, fetchDepositNetworks, fetchOrderBookDetails, fetchPositionFundingSince, fetchPublicAccount, fetchPublicOrderBookDetails, fetchTradesSince, fetchUserReferrals, fetchWithdrawHistory, submitReferralCode };
|
|
528
|
+
export { LighterRequestError, createCctpDepositIntentAddress, fetchAccount, fetchAccountLimits, fetchAccountMetadata, fetchAccountsByL1Address, fetchApiKeys, fetchAssetDetails, fetchDepositHistory, fetchDepositNetworks, fetchOrderBookDetails, fetchPositionFundingSince, fetchPublicAccount, fetchPublicOrderBookDetails, fetchTradesSince, fetchUserReferrals, fetchWithdrawHistory, submitReferralCode };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,30 @@ import { WalletClient, Transport, Chain, Account } from 'viem';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
|
|
8
8
|
type SupportedCredentials = HttpTypes.ExchangeCredentialsResponse['credentials'];
|
|
9
|
+
/**
|
|
10
|
+
* Optional pacing seam around the syncer's exchange info requests. The SDK stays
|
|
11
|
+
* rate-limit-agnostic: it only exposes when a request starts, what it returned,
|
|
12
|
+
* and when it failed — the caller owns all budget math and waiting.
|
|
13
|
+
*/
|
|
14
|
+
type SyncerThrottle = {
|
|
15
|
+
/** Awaited before the request fires; the caller blocks here until its budget allows the call. */
|
|
16
|
+
beforeRequest(requestType: string): Promise<void>;
|
|
17
|
+
/** Called after a successful response; `itemCount` is the response array length (0 for object responses). */
|
|
18
|
+
afterRequest(requestType: string, itemCount: number): void;
|
|
19
|
+
/** Called when the request threw, before the error propagates; lets the caller inspect rate-limit rejections. */
|
|
20
|
+
afterRequestError(requestType: string, error: unknown): void;
|
|
21
|
+
};
|
|
9
22
|
type ExchangesSDKConfig = {
|
|
10
23
|
sdk: PearSDK;
|
|
11
24
|
demo?: boolean;
|
|
25
|
+
/** Optional pacing seam around the syncer's exchange info requests; absent = unthrottled (current behavior). */
|
|
26
|
+
throttle?: SyncerThrottle;
|
|
27
|
+
/**
|
|
28
|
+
* Optional pre-fetched Hyperliquid perp-dex names ('' = the main dex). The list is
|
|
29
|
+
* global (identical for every account), so a batch caller fetches it once per run and
|
|
30
|
+
* passes it here; absent = the syncer fetches it per sync (current behavior).
|
|
31
|
+
*/
|
|
32
|
+
hyperliquidPerpDexNames?: string[];
|
|
12
33
|
/**
|
|
13
34
|
* Optional forward-proxy for the exchange REST calls. When enabled, requests are re-pointed at
|
|
14
35
|
* `proxy.url` with the original venue host in `X-Proxy-Target`. Injected here because the SDK
|
|
@@ -918,6 +939,10 @@ interface AccountSyncerConfig {
|
|
|
918
939
|
account: TradeAccount;
|
|
919
940
|
credentials: SupportedCredentials;
|
|
920
941
|
demo: boolean;
|
|
942
|
+
/** Optional pacing seam around the exchange info requests; absent = unthrottled (current behavior). */
|
|
943
|
+
throttle?: SyncerThrottle;
|
|
944
|
+
/** Optional pre-fetched Hyperliquid perp-dex names ('' = the main dex); absent = fetched per sync. */
|
|
945
|
+
hyperliquidPerpDexNames?: string[];
|
|
921
946
|
}
|
|
922
947
|
/**
|
|
923
948
|
* Outcome of one fill sync cycle, so batch/cron callers can tell a completed
|
|
@@ -959,6 +984,7 @@ declare abstract class BaseAccountSyncer<ExternalFill> implements AccountSyncer
|
|
|
959
984
|
protected account: TradeAccount;
|
|
960
985
|
protected credentials: SupportedCredentials;
|
|
961
986
|
protected demo: boolean;
|
|
987
|
+
protected throttle: SyncerThrottle | undefined;
|
|
962
988
|
constructor(config: AccountSyncerConfig);
|
|
963
989
|
/**
|
|
964
990
|
* Fetch the account's currently-open positions from the exchange, normalized
|
|
@@ -1125,11 +1151,19 @@ type UserFillsByTimeResponse = {
|
|
|
1125
1151
|
type HyperliquidRawFill = UserFillsByTimeResponse;
|
|
1126
1152
|
declare class HyperliquidAccountSyncer extends BaseAccountSyncer<HyperliquidRawFill> {
|
|
1127
1153
|
private readonly address;
|
|
1154
|
+
private readonly perpDexNames;
|
|
1128
1155
|
constructor(config: AccountSyncerConfig);
|
|
1129
1156
|
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
1130
1157
|
protected listExternalFills({ since, instruments, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
1131
1158
|
protected listExternalFunding({ since, instruments, }: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
1132
1159
|
protected isReduceOnly(rawFill: HyperliquidRawFill): boolean;
|
|
1160
|
+
/** Fetch the perp-dex descriptors and reduce them to unique dex names ('' = the main dex). */
|
|
1161
|
+
private fetchPerpDexNames;
|
|
1162
|
+
/**
|
|
1163
|
+
* Route one info request through the optional throttle seam: gate before the
|
|
1164
|
+
* call, report the response size after, surface a failure before it propagates.
|
|
1165
|
+
*/
|
|
1166
|
+
private fetchInfoGated;
|
|
1133
1167
|
}
|
|
1134
1168
|
|
|
1135
1169
|
type RawFill$1 = LighterRawTrade;
|
|
@@ -1211,6 +1245,7 @@ declare const AccountBalance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1211
1245
|
stakedEquity: z.ZodNullable<z.ZodString>;
|
|
1212
1246
|
unrealizedPnl: z.ZodString;
|
|
1213
1247
|
maintenanceMargin: z.ZodString;
|
|
1248
|
+
marginUsed: z.ZodString;
|
|
1214
1249
|
crossMarginRatio: z.ZodString;
|
|
1215
1250
|
crossAccountLeverage: z.ZodString;
|
|
1216
1251
|
perDexState: z.ZodArray<z.ZodObject<{
|
|
@@ -1240,6 +1275,7 @@ declare const AccountBalance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1240
1275
|
portfolioValue: z.ZodNullable<z.ZodString>;
|
|
1241
1276
|
unrealizedPnl: z.ZodString;
|
|
1242
1277
|
perpsMaintenanceMargin: z.ZodString;
|
|
1278
|
+
marginUsed: z.ZodString;
|
|
1243
1279
|
unifiedAccountRatio: z.ZodString;
|
|
1244
1280
|
unifiedAccountLeverage: z.ZodNullable<z.ZodString>;
|
|
1245
1281
|
perDexState: z.ZodArray<z.ZodObject<{
|
|
@@ -1271,6 +1307,7 @@ declare const AccountBalance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1271
1307
|
portfolioValue: z.ZodNullable<z.ZodString>;
|
|
1272
1308
|
unrealizedPnl: z.ZodString;
|
|
1273
1309
|
perpsMaintenanceMargin: z.ZodString;
|
|
1310
|
+
marginUsed: z.ZodString;
|
|
1274
1311
|
portfolioMarginRatio: z.ZodNullable<z.ZodString>;
|
|
1275
1312
|
portfolioAccountLeverage: z.ZodNullable<z.ZodString>;
|
|
1276
1313
|
borrowCapUsed: z.ZodNullable<z.ZodString>;
|
|
@@ -1301,6 +1338,7 @@ declare const AccountBalance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1301
1338
|
stakingEquity: z.ZodNullable<z.ZodString>;
|
|
1302
1339
|
creditedInLit: z.ZodNullable<z.ZodString>;
|
|
1303
1340
|
unrealizedPnl: z.ZodNullable<z.ZodString>;
|
|
1341
|
+
marginUsed: z.ZodNullable<z.ZodString>;
|
|
1304
1342
|
crossLeverage: z.ZodNullable<z.ZodString>;
|
|
1305
1343
|
maintenanceMargin: z.ZodNullable<z.ZodString>;
|
|
1306
1344
|
crossMarginUsage: z.ZodNullable<z.ZodString>;
|
|
@@ -1326,6 +1364,7 @@ declare const AccountBalance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1326
1364
|
stakingEquity: z.ZodNullable<z.ZodString>;
|
|
1327
1365
|
creditedInLit: z.ZodNullable<z.ZodString>;
|
|
1328
1366
|
unrealizedPnl: z.ZodNullable<z.ZodString>;
|
|
1367
|
+
marginUsed: z.ZodNullable<z.ZodString>;
|
|
1329
1368
|
crossLeverage: z.ZodNullable<z.ZodString>;
|
|
1330
1369
|
maintenanceMargin: z.ZodNullable<z.ZodString>;
|
|
1331
1370
|
crossMarginUsage: z.ZodNullable<z.ZodString>;
|
|
@@ -1532,6 +1571,20 @@ interface AccountPosition {
|
|
|
1532
1571
|
size: string;
|
|
1533
1572
|
entryPrice: string;
|
|
1534
1573
|
unrealizedPnl: string;
|
|
1574
|
+
/**
|
|
1575
|
+
* The margin this one position holds, in the account's quote currency.
|
|
1576
|
+
*
|
|
1577
|
+
* The meaning is the venue's own, and the two margin modes state different things: an isolated
|
|
1578
|
+
* leg holds collateral of its own, while a cross position holds the initial margin its market
|
|
1579
|
+
* demands out of the shared pool.
|
|
1580
|
+
*
|
|
1581
|
+
* `null` where the venue publishes no per-position figure and none can be derived from what it
|
|
1582
|
+
* does publish — the margin is then unknown, which is not the same as a position holding none.
|
|
1583
|
+
*
|
|
1584
|
+
* The currency is the one that position's own venue settles in, and no field states which: a
|
|
1585
|
+
* Hyperliquid position on a non-USDC HIP-3 dex holds margin in that dex's collateral token.
|
|
1586
|
+
*/
|
|
1587
|
+
marginUsed: string | null;
|
|
1535
1588
|
leverage: string;
|
|
1536
1589
|
marginType: MarginMode;
|
|
1537
1590
|
liquidationPrice: string | null;
|
|
@@ -1919,6 +1972,7 @@ declare const HyperliquidStandardBalance: z.ZodObject<{
|
|
|
1919
1972
|
stakedEquity: z.ZodNullable<z.ZodString>;
|
|
1920
1973
|
unrealizedPnl: z.ZodString;
|
|
1921
1974
|
maintenanceMargin: z.ZodString;
|
|
1975
|
+
marginUsed: z.ZodString;
|
|
1922
1976
|
crossMarginRatio: z.ZodString;
|
|
1923
1977
|
crossAccountLeverage: z.ZodString;
|
|
1924
1978
|
perDexState: z.ZodArray<z.ZodObject<{
|
|
@@ -1950,6 +2004,7 @@ declare const HyperliquidUnifiedBalance: z.ZodObject<{
|
|
|
1950
2004
|
portfolioValue: z.ZodNullable<z.ZodString>;
|
|
1951
2005
|
unrealizedPnl: z.ZodString;
|
|
1952
2006
|
perpsMaintenanceMargin: z.ZodString;
|
|
2007
|
+
marginUsed: z.ZodString;
|
|
1953
2008
|
unifiedAccountRatio: z.ZodString;
|
|
1954
2009
|
unifiedAccountLeverage: z.ZodNullable<z.ZodString>;
|
|
1955
2010
|
perDexState: z.ZodArray<z.ZodObject<{
|
|
@@ -2009,6 +2064,7 @@ declare const HyperliquidPortfolioMarginBalance: z.ZodObject<{
|
|
|
2009
2064
|
portfolioValue: z.ZodNullable<z.ZodString>;
|
|
2010
2065
|
unrealizedPnl: z.ZodString;
|
|
2011
2066
|
perpsMaintenanceMargin: z.ZodString;
|
|
2067
|
+
marginUsed: z.ZodString;
|
|
2012
2068
|
portfolioMarginRatio: z.ZodNullable<z.ZodString>;
|
|
2013
2069
|
portfolioAccountLeverage: z.ZodNullable<z.ZodString>;
|
|
2014
2070
|
borrowCapUsed: z.ZodNullable<z.ZodString>;
|
|
@@ -2143,6 +2199,7 @@ declare const LighterClassicBalance: z.ZodObject<{
|
|
|
2143
2199
|
stakingEquity: z.ZodNullable<z.ZodString>;
|
|
2144
2200
|
creditedInLit: z.ZodNullable<z.ZodString>;
|
|
2145
2201
|
unrealizedPnl: z.ZodNullable<z.ZodString>;
|
|
2202
|
+
marginUsed: z.ZodNullable<z.ZodString>;
|
|
2146
2203
|
crossLeverage: z.ZodNullable<z.ZodString>;
|
|
2147
2204
|
maintenanceMargin: z.ZodNullable<z.ZodString>;
|
|
2148
2205
|
crossMarginUsage: z.ZodNullable<z.ZodString>;
|
|
@@ -2170,6 +2227,7 @@ declare const LighterUnifiedBalance: z.ZodObject<{
|
|
|
2170
2227
|
stakingEquity: z.ZodNullable<z.ZodString>;
|
|
2171
2228
|
creditedInLit: z.ZodNullable<z.ZodString>;
|
|
2172
2229
|
unrealizedPnl: z.ZodNullable<z.ZodString>;
|
|
2230
|
+
marginUsed: z.ZodNullable<z.ZodString>;
|
|
2173
2231
|
crossLeverage: z.ZodNullable<z.ZodString>;
|
|
2174
2232
|
maintenanceMargin: z.ZodNullable<z.ZodString>;
|
|
2175
2233
|
crossMarginUsage: z.ZodNullable<z.ZodString>;
|
|
@@ -2202,9 +2260,9 @@ declare class LighterAccountTracker extends BaseAccountTracker {
|
|
|
2202
2260
|
private startInFlight;
|
|
2203
2261
|
private lifecycleVersion;
|
|
2204
2262
|
private wsConnectionVersion;
|
|
2205
|
-
private
|
|
2263
|
+
private positionStateVersion;
|
|
2206
2264
|
private assetsByAssetId;
|
|
2207
|
-
private
|
|
2265
|
+
private positionsByMarketId;
|
|
2208
2266
|
private marketsByMarketId;
|
|
2209
2267
|
private marketsBySymbol;
|
|
2210
2268
|
private spotPriceBySymbol;
|
|
@@ -2253,10 +2311,10 @@ declare class LighterAccountTracker extends BaseAccountTracker {
|
|
|
2253
2311
|
/**
|
|
2254
2312
|
* Subscribes this socket epoch's account channels, and keeps trying until one attempt lands.
|
|
2255
2313
|
*
|
|
2256
|
-
* This is the sole subscriber of `account_all` and `user_stats`, and
|
|
2257
|
-
*
|
|
2258
|
-
*
|
|
2259
|
-
*
|
|
2314
|
+
* This is the sole subscriber of `account_all` and `user_stats`, and no balance exists without
|
|
2315
|
+
* both — see `buildBalance`. A mint that failed once and was never retried therefore left the
|
|
2316
|
+
* tracker connected but permanently unsubscribed: the 2s REST poll kept positions moving while
|
|
2317
|
+
* the balance stayed null until some incidental reconnect.
|
|
2260
2318
|
*
|
|
2261
2319
|
* The wait doubles up to a ceiling, and only a new connection epoch or teardown ends it.
|
|
2262
2320
|
*/
|
|
@@ -2271,13 +2329,15 @@ declare class LighterAccountTracker extends BaseAccountTracker {
|
|
|
2271
2329
|
/** Keeps both published scopes: cross drives every "Cross" field, total drives the account ones. */
|
|
2272
2330
|
private onUserStats;
|
|
2273
2331
|
/**
|
|
2274
|
-
* The account's balance,
|
|
2332
|
+
* The account's balance, from the freshest state each of its parts has.
|
|
2275
2333
|
*
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
*
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2334
|
+
* The two account scopes come from `user_stats` and the assets from `account_all`, both
|
|
2335
|
+
* websocket-only. The positions come from whichever source read the account last — a frame, or
|
|
2336
|
+
* the REST poll that keeps them moving while the socket says nothing (see
|
|
2337
|
+
* `performPositionPoll`). The market context is venue-wide metadata on its own timer.
|
|
2338
|
+
*
|
|
2339
|
+
* This is the ONLY producer of the value `getBalance()` returns while the tracker is running;
|
|
2340
|
+
* `snapshot()` is the separate one-shot path and never writes here.
|
|
2281
2341
|
*
|
|
2282
2342
|
* Null until both channels have delivered a frame, because a balance built from one of them
|
|
2283
2343
|
* would be missing half the account.
|
|
@@ -2295,16 +2355,27 @@ declare class LighterAccountTracker extends BaseAccountTracker {
|
|
|
2295
2355
|
private retrieveRequiredAuthToken;
|
|
2296
2356
|
private pollPositions;
|
|
2297
2357
|
/**
|
|
2298
|
-
* Republishes positions
|
|
2358
|
+
* Republishes positions, per-market settings, and the balance those positions move.
|
|
2359
|
+
*
|
|
2360
|
+
* This is what keeps the account's open result alive. Lighter republishes `account_all` when the
|
|
2361
|
+
* account itself changes and not when the mark moves, so between fills the position rows hold
|
|
2362
|
+
* whatever the last frame left them at. A balance built on those rows stated a dead unrealized
|
|
2363
|
+
* PnL and a dead maintenance margin beside the live `user_stats` figures — and restated them on
|
|
2364
|
+
* every frame under a current timestamp, so nothing marked them as old. The venue's own account
|
|
2365
|
+
* read is the only source that carries the new figures, so the poll adopts it wholesale.
|
|
2366
|
+
*
|
|
2367
|
+
* The rows land in the same store `account_all` writes, which is what keeps `getPositions()` and
|
|
2368
|
+
* the balance's `unrealizedPnl` stating one account rather than two.
|
|
2299
2369
|
*
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
2303
|
-
*
|
|
2370
|
+
* A read is dropped whole when a frame moved the position state while it was in flight: that
|
|
2371
|
+
* frame is the newer account. `user_stats` deliberately does not invalidate one — it carries the
|
|
2372
|
+
* account scopes and no positions at all, and a venue publishing it every tick would otherwise
|
|
2373
|
+
* cancel every poll and freeze the very figures this read exists to move.
|
|
2304
2374
|
*
|
|
2305
|
-
*
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2375
|
+
* No balance leaves here while the socket is down. The account scopes and the assets are dropped
|
|
2376
|
+
* on the next open rather than on the drop, so a balance published mid-outage would pair these
|
|
2377
|
+
* fresh positions with figures frozen at the drop — worst of it a cross margin ratio measured
|
|
2378
|
+
* against a portfolio value the account no longer has. Positions still publish: they are whole.
|
|
2308
2379
|
*/
|
|
2309
2380
|
private performPositionPoll;
|
|
2310
2381
|
/**
|
|
@@ -2531,6 +2602,8 @@ declare function createAccountTracker(exchange: Connector, config: CreateAccount
|
|
|
2531
2602
|
declare class ExchangesSDK {
|
|
2532
2603
|
private sdk;
|
|
2533
2604
|
private demo;
|
|
2605
|
+
private throttle;
|
|
2606
|
+
private hyperliquidPerpDexNames;
|
|
2534
2607
|
constructor(config: ExchangesSDKConfig);
|
|
2535
2608
|
connect(tradeAccountId: string): Promise<ExchangeConnection>;
|
|
2536
2609
|
/**
|
|
@@ -2548,4 +2621,4 @@ declare class ExchangesSDK {
|
|
|
2548
2621
|
private fetchCredentials;
|
|
2549
2622
|
}
|
|
2550
2623
|
|
|
2551
|
-
export { ACCOUNT_MODE_LABELS, ASSET_DECIMALS, AccountBalance, AccountBalanceCore, type AccountHealthStatus, AccountMode, type AccountPosition, type AccountSnapshot, type AccountStatus, type AccountSyncer, type AccountSyncerConfig, type AccountTierCallback, type AccountTracker, type AccountTrackerConfig, type AccountWarningReason, type ApiKey, type ApproveIntegratorParams, type ApproveIntegratorResult, AssetId, type AssetSnapshotMap, type BalanceCallback, BaseAccountSyncer, BaseAccountTracker, BaseWs, BinanceAccountSyncer, BinanceAccountTracker, type BinanceAccountTrackerConfig, BinanceExchangeWs, BinanceMultiAssetBalance, BinanceSingleAssetBalance, BinanceUserDataWs, type BinanceUserDataWsConfig, BinanceWsApi, type BinanceWsApiConfig, BybitAccountSyncer, BybitAccountTracker, type BybitAccountTrackerConfig, BybitExchangeWs, BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance, BybitWs, type BybitWsConfig, type CctpDepositAddressParams, type CctpDepositAddressResult, type CctpDepositHistorySettlement, type CctpDepositParams, type CctpDepositRegistrationSettlement, type CctpDepositResult, type CctpDepositSettlement, type ChangeAccountTierParams, type ChangeAccountTierResult, type CheckApiWalletParams, CollateralBalance, type ConnectOptions, type ConnectWalletParams, type ConnectWalletResult, type CreateAccountTrackerConfig, type CreateExchangeWsConfig, DecimalString, type EthereumDepositParams, type EthereumDepositResult, type ExchangeConnection, type ExchangeHelperExchange, type ExchangeHelperMap, type ExchangeState, ExchangeWs, type ExchangeWsByConnector, ExchangesSDK, type ExchangesSDKConfig, FUNXYZ_DEPOSIT_MINIMUMS_USD, FUNXYZ_ETHEREUM_MAINNET_CHAIN_ID, FUNXYZ_EVM_CHAIN_NAMES, type FastWithdrawParams, type FastWithdrawResult, type FillSyncOutcome, type FundingSyncOutcome, type FunxyzAccountState, type FunxyzAsset, type FunxyzDepositAddressParams, type FunxyzDepositAddressResult, type FunxyzDepositCredit, type FunxyzDepositParams, type FunxyzDepositResult, type FunxyzDepositSettlement, type FunxyzDepositSource, type FunxyzMarket, HyperliquidAccountSyncer, HyperliquidAccountTracker, type HyperliquidAccountTrackerConfig, HyperliquidDexState, HyperliquidExchangeWs, type HyperliquidHelper, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance, HyperliquidWs, type IntegratorApprovalRequirements, L2_ASSET_DECIMALS, LIGHTER_NATIVE_USDC_ADDRESSES, LIGHTER_USDC_DECIMALS, LighterAccountSyncer, type LighterAccountTier, LighterAccountTracker, type LighterAccountTrackerConfig, type LighterApplyReferralCodeParams, type LighterAuthCredentials, LighterClassicBalance, type LighterDeposit, type LighterDepositAddressParams, type LighterDepositAddressResult, type LighterDepositCredit, type LighterDepositHistoryParams, type LighterDepositNetwork, type LighterDepositPage, type LighterDepositParams, type LighterDepositProgress, type LighterDepositResult, type LighterDepositStatus, type LighterDepositWalletAdapter, LighterExchangeWs, type LighterHelper, type LighterLookupResult, type LighterOnboardingStatus, type LighterReferral, type LighterReferralTotals, type LighterReferralTradeStats, LighterUnifiedBalance, type LighterUserReferrals, type LighterUserReferralsParams, type LighterWithdraw, type LighterWithdrawHistoryParams, type LighterWithdrawPage, type LighterWithdrawStatus, type LighterWithdrawType, LighterWs, type ListExternalFillsParams, type ListExternalFundingParams, type ListExternalPositionsParams, MAX_API_KEY_INDEX, MIN_LIGHTER_CCTP_DEPOSIT_USDC, MIN_LIGHTER_FAST_WITHDRAW_USDC, MIN_USER_API_KEY_INDEX, MarginMode, type MessageListener, OkxAccountSyncer, OkxAccountTracker, type OkxAccountTrackerConfig, OkxExchangeWs, OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance, OkxWs, type OkxWsConfig, type OpenListener, type PositionsCallback, RouteType, Side, SignedDecimalString, type StateUpdate, type SupportedCredentials, type TrackedAssetCallback, type TrackedAssetInfo, type WalletClientWithAccount, type WithdrawParams, type WithdrawResult, type WsMessage, createAccountSyncer, createAccountTracker, createExchangeWs, createHyperliquidHelper, createLighterHelper, findAvailableApiKeyIndex, getAssetDecimals, getLighterNativeUsdcAddress, listFunxyzTransferableChains, readFunxyzChainSource, readFunxyzMinimumUsd, retrieveLighterAuthToken, usdcAbi };
|
|
2624
|
+
export { ACCOUNT_MODE_LABELS, ASSET_DECIMALS, AccountBalance, AccountBalanceCore, type AccountHealthStatus, AccountMode, type AccountPosition, type AccountSnapshot, type AccountStatus, type AccountSyncer, type AccountSyncerConfig, type AccountTierCallback, type AccountTracker, type AccountTrackerConfig, type AccountWarningReason, type ApiKey, type ApproveIntegratorParams, type ApproveIntegratorResult, AssetId, type AssetSnapshotMap, type BalanceCallback, BaseAccountSyncer, BaseAccountTracker, BaseWs, BinanceAccountSyncer, BinanceAccountTracker, type BinanceAccountTrackerConfig, BinanceExchangeWs, BinanceMultiAssetBalance, BinanceSingleAssetBalance, BinanceUserDataWs, type BinanceUserDataWsConfig, BinanceWsApi, type BinanceWsApiConfig, BybitAccountSyncer, BybitAccountTracker, type BybitAccountTrackerConfig, BybitExchangeWs, BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance, BybitWs, type BybitWsConfig, type CctpDepositAddressParams, type CctpDepositAddressResult, type CctpDepositHistorySettlement, type CctpDepositParams, type CctpDepositRegistrationSettlement, type CctpDepositResult, type CctpDepositSettlement, type ChangeAccountTierParams, type ChangeAccountTierResult, type CheckApiWalletParams, CollateralBalance, type ConnectOptions, type ConnectWalletParams, type ConnectWalletResult, type CreateAccountTrackerConfig, type CreateExchangeWsConfig, DecimalString, type EthereumDepositParams, type EthereumDepositResult, type ExchangeConnection, type ExchangeHelperExchange, type ExchangeHelperMap, type ExchangeState, ExchangeWs, type ExchangeWsByConnector, ExchangesSDK, type ExchangesSDKConfig, FUNXYZ_DEPOSIT_MINIMUMS_USD, FUNXYZ_ETHEREUM_MAINNET_CHAIN_ID, FUNXYZ_EVM_CHAIN_NAMES, type FastWithdrawParams, type FastWithdrawResult, type FillSyncOutcome, type FundingSyncOutcome, type FunxyzAccountState, type FunxyzAsset, type FunxyzDepositAddressParams, type FunxyzDepositAddressResult, type FunxyzDepositCredit, type FunxyzDepositParams, type FunxyzDepositResult, type FunxyzDepositSettlement, type FunxyzDepositSource, type FunxyzMarket, HyperliquidAccountSyncer, HyperliquidAccountTracker, type HyperliquidAccountTrackerConfig, HyperliquidDexState, HyperliquidExchangeWs, type HyperliquidHelper, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance, HyperliquidWs, type IntegratorApprovalRequirements, L2_ASSET_DECIMALS, LIGHTER_NATIVE_USDC_ADDRESSES, LIGHTER_USDC_DECIMALS, LighterAccountSyncer, type LighterAccountTier, LighterAccountTracker, type LighterAccountTrackerConfig, type LighterApplyReferralCodeParams, type LighterAuthCredentials, LighterClassicBalance, type LighterDeposit, type LighterDepositAddressParams, type LighterDepositAddressResult, type LighterDepositCredit, type LighterDepositHistoryParams, type LighterDepositNetwork, type LighterDepositPage, type LighterDepositParams, type LighterDepositProgress, type LighterDepositResult, type LighterDepositStatus, type LighterDepositWalletAdapter, LighterExchangeWs, type LighterHelper, type LighterLookupResult, type LighterOnboardingStatus, type LighterReferral, type LighterReferralTotals, type LighterReferralTradeStats, LighterUnifiedBalance, type LighterUserReferrals, type LighterUserReferralsParams, type LighterWithdraw, type LighterWithdrawHistoryParams, type LighterWithdrawPage, type LighterWithdrawStatus, type LighterWithdrawType, LighterWs, type ListExternalFillsParams, type ListExternalFundingParams, type ListExternalPositionsParams, MAX_API_KEY_INDEX, MIN_LIGHTER_CCTP_DEPOSIT_USDC, MIN_LIGHTER_FAST_WITHDRAW_USDC, MIN_USER_API_KEY_INDEX, MarginMode, type MessageListener, OkxAccountSyncer, OkxAccountTracker, type OkxAccountTrackerConfig, OkxExchangeWs, OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance, OkxWs, type OkxWsConfig, type OpenListener, type PositionsCallback, RouteType, Side, SignedDecimalString, type StateUpdate, type SupportedCredentials, type SyncerThrottle, type TrackedAssetCallback, type TrackedAssetInfo, type WalletClientWithAccount, type WithdrawParams, type WithdrawResult, type WsMessage, createAccountSyncer, createAccountTracker, createExchangeWs, createHyperliquidHelper, createLighterHelper, findAvailableApiKeyIndex, getAssetDecimals, getLighterNativeUsdcAddress, listFunxyzTransferableChains, readFunxyzChainSource, readFunxyzMinimumUsd, retrieveLighterAuthToken, usdcAbi };
|
package/dist/sdk.js
CHANGED
|
@@ -12,9 +12,13 @@ class ExchangesSDK {
|
|
|
12
12
|
}
|
|
13
13
|
sdk;
|
|
14
14
|
demo;
|
|
15
|
+
throttle;
|
|
16
|
+
hyperliquidPerpDexNames;
|
|
15
17
|
constructor(config) {
|
|
16
18
|
this.sdk = config.sdk;
|
|
17
19
|
this.demo = config.demo ?? false;
|
|
20
|
+
this.throttle = config.throttle;
|
|
21
|
+
this.hyperliquidPerpDexNames = config.hyperliquidPerpDexNames;
|
|
18
22
|
if (config.proxy) configureProxy(config.proxy);
|
|
19
23
|
}
|
|
20
24
|
async connect(tradeAccountId) {
|
|
@@ -67,7 +71,9 @@ class ExchangesSDK {
|
|
|
67
71
|
connector: connection.connector,
|
|
68
72
|
account: connection.account,
|
|
69
73
|
credentials: connection.credentials,
|
|
70
|
-
demo: this.demo
|
|
74
|
+
demo: this.demo,
|
|
75
|
+
throttle: this.throttle,
|
|
76
|
+
hyperliquidPerpDexNames: this.hyperliquidPerpDexNames
|
|
71
77
|
});
|
|
72
78
|
}
|
|
73
79
|
buildTracker(connection) {
|
package/dist/syncer/base.js
CHANGED
|
@@ -12,12 +12,14 @@ class BaseAccountSyncer {
|
|
|
12
12
|
account;
|
|
13
13
|
credentials;
|
|
14
14
|
demo;
|
|
15
|
+
throttle;
|
|
15
16
|
constructor(config) {
|
|
16
17
|
this.sdk = config.sdk;
|
|
17
18
|
this.connector = config.connector;
|
|
18
19
|
this.account = config.account;
|
|
19
20
|
this.credentials = config.credentials;
|
|
20
21
|
this.demo = config.demo;
|
|
22
|
+
this.throttle = config.throttle;
|
|
21
23
|
}
|
|
22
24
|
// Phase A: Load current Pear state (positions, fills, instruments, watermark).
|
|
23
25
|
async loadPearState() {
|
|
@@ -11,9 +11,11 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
11
11
|
__name(this, "HyperliquidAccountSyncer");
|
|
12
12
|
}
|
|
13
13
|
address;
|
|
14
|
+
perpDexNames;
|
|
14
15
|
constructor(config) {
|
|
15
16
|
super(config);
|
|
16
17
|
this.address = config.account.exchangeIdentifier;
|
|
18
|
+
this.perpDexNames = config.hyperliquidPerpDexNames;
|
|
17
19
|
}
|
|
18
20
|
async listExternalPositions({ instruments }) {
|
|
19
21
|
const instrumentByBase = new Map(instruments.map((instrument) => [
|
|
@@ -23,17 +25,12 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
23
25
|
contractSize: instrument.precision.quantity.contractSize
|
|
24
26
|
}
|
|
25
27
|
]));
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
}, this.demo);
|
|
29
|
-
const dexNames = [
|
|
30
|
-
...new Set(perpDexs.map((perpDex) => perpDex?.name ?? ""))
|
|
31
|
-
];
|
|
32
|
-
const states = await Promise.all(dexNames.map((dex) => fetchInfo({
|
|
28
|
+
const dexNames = this.perpDexNames ?? await this.fetchPerpDexNames();
|
|
29
|
+
const states = await Promise.all(dexNames.map((dex) => this.fetchInfoGated({
|
|
33
30
|
type: "clearinghouseState",
|
|
34
31
|
user: this.address,
|
|
35
32
|
dex
|
|
36
|
-
}
|
|
33
|
+
})));
|
|
37
34
|
const assetPositions = states.flatMap((state) => state.assetPositions);
|
|
38
35
|
return assetPositions.reduce((positions, { position: rawPosition }) => {
|
|
39
36
|
const szi = new BigNumber(rawPosition.szi);
|
|
@@ -72,12 +69,12 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
72
69
|
let cursor = since.getTime();
|
|
73
70
|
let batchSize;
|
|
74
71
|
do {
|
|
75
|
-
const batch = await
|
|
72
|
+
const batch = await this.fetchInfoGated({
|
|
76
73
|
type: "userFillsByTime",
|
|
77
74
|
user: this.address,
|
|
78
75
|
startTime: cursor,
|
|
79
76
|
aggregateByTime: true
|
|
80
|
-
}
|
|
77
|
+
});
|
|
81
78
|
batchSize = batch.length;
|
|
82
79
|
for (const rawFill of batch) {
|
|
83
80
|
addFill(rawFill);
|
|
@@ -93,12 +90,12 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
93
90
|
} while (batchSize === HYPERLIQUID_FILL_PAGE_LIMIT);
|
|
94
91
|
cursor = since.getTime();
|
|
95
92
|
do {
|
|
96
|
-
const twapBatch = await
|
|
93
|
+
const twapBatch = await this.fetchInfoGated({
|
|
97
94
|
type: "userTwapSliceFillsByTime",
|
|
98
95
|
user: this.address,
|
|
99
96
|
startTime: cursor,
|
|
100
97
|
aggregateByTime: true
|
|
101
|
-
}
|
|
98
|
+
});
|
|
102
99
|
batchSize = twapBatch.length;
|
|
103
100
|
for (const item of twapBatch) {
|
|
104
101
|
addFill(item.fill);
|
|
@@ -149,11 +146,11 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
149
146
|
let cursor = since.getTime();
|
|
150
147
|
let batchSize;
|
|
151
148
|
do {
|
|
152
|
-
const batch = await
|
|
149
|
+
const batch = await this.fetchInfoGated({
|
|
153
150
|
type: "userFunding",
|
|
154
151
|
user: this.address,
|
|
155
152
|
startTime: cursor
|
|
156
|
-
}
|
|
153
|
+
});
|
|
157
154
|
batchSize = batch.length;
|
|
158
155
|
for (const event of batch) {
|
|
159
156
|
allEvents.set(`${event.time}:${event.delta.coin}:${event.hash}`, event);
|
|
@@ -174,6 +171,33 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
174
171
|
isReduceOnly(rawFill) {
|
|
175
172
|
return rawFill.dir.includes("Close");
|
|
176
173
|
}
|
|
174
|
+
/** Fetch the perp-dex descriptors and reduce them to unique dex names ('' = the main dex). */
|
|
175
|
+
async fetchPerpDexNames() {
|
|
176
|
+
const perpDexs = await this.fetchInfoGated({
|
|
177
|
+
type: "perpDexs"
|
|
178
|
+
});
|
|
179
|
+
return [
|
|
180
|
+
...new Set(perpDexs.map((perpDex) => perpDex?.name ?? ""))
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Route one info request through the optional throttle seam: gate before the
|
|
185
|
+
* call, report the response size after, surface a failure before it propagates.
|
|
186
|
+
*/
|
|
187
|
+
async fetchInfoGated(body) {
|
|
188
|
+
if (!this.throttle) {
|
|
189
|
+
return fetchInfo(body, this.demo);
|
|
190
|
+
}
|
|
191
|
+
await this.throttle.beforeRequest(body.type);
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetchInfo(body, this.demo);
|
|
194
|
+
this.throttle.afterRequest(body.type, Array.isArray(response) ? response.length : 0);
|
|
195
|
+
return response;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.throttle.afterRequestError(body.type, error);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
177
201
|
}
|
|
178
202
|
function warnStalledPagination(stream, cursor) {
|
|
179
203
|
console.warn(`Hyperliquid ${stream} pagination stalled at timestamp ${cursor}; returned history may be incomplete.`);
|
|
@@ -36,7 +36,7 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
36
36
|
instrument.precision.quantity.contractSize
|
|
37
37
|
]));
|
|
38
38
|
const token = await this.retrieveAuthToken();
|
|
39
|
-
const response = await fetchAccount(this.accountIndex, token, this.demo);
|
|
39
|
+
const response = await fetchAccount(this.accountIndex, token, this.demo, this.throttle);
|
|
40
40
|
const rawPositions = response.accounts?.[0]?.positions ?? [];
|
|
41
41
|
return rawPositions.reduce((positions, rawPosition) => {
|
|
42
42
|
const instrumentId = rawPosition.market_id.toString();
|
|
@@ -59,7 +59,7 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
59
59
|
async listExternalFills({ since, instruments }) {
|
|
60
60
|
this.assertAuthenticatedSyncAvailable();
|
|
61
61
|
const token = await this.retrieveAuthToken();
|
|
62
|
-
const trades = await fetchTradesSince(this.accountIndex, token, since, this.demo);
|
|
62
|
+
const trades = await fetchTradesSince(this.accountIndex, token, since, this.demo, this.throttle);
|
|
63
63
|
const instrumentIds = new Set(instruments.map((instrument) => instrument.id));
|
|
64
64
|
return trades.toSorted((a, b) => a.timestamp - b.timestamp).reduce((fills, rawFill) => {
|
|
65
65
|
const instrumentId = rawFill.market_id.toString();
|
|
@@ -92,7 +92,7 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
92
92
|
async listExternalFunding({ since, instruments }) {
|
|
93
93
|
this.assertAuthenticatedSyncAvailable();
|
|
94
94
|
const token = await this.retrieveAuthToken();
|
|
95
|
-
const fundingEvents = await fetchPositionFundingSince(this.accountIndex, token, since, this.demo);
|
|
95
|
+
const fundingEvents = await fetchPositionFundingSince(this.accountIndex, token, since, this.demo, this.throttle);
|
|
96
96
|
const instrumentIds = new Set(instruments.map((instrument) => instrument.id));
|
|
97
97
|
return toFundingPaymentEntries(fundingEvents, instrumentIds);
|
|
98
98
|
}
|
|
@@ -54,6 +54,8 @@ function mapPositions(raw) {
|
|
|
54
54
|
size: amt,
|
|
55
55
|
entryPrice: p.entryPrice ?? "0",
|
|
56
56
|
unrealizedPnl: p.unRealizedProfit ?? "0",
|
|
57
|
+
// Binance states `initialMargin` per position; mapping it is left to its own change.
|
|
58
|
+
marginUsed: null,
|
|
57
59
|
leverage: p.leverage ?? "0",
|
|
58
60
|
marginType: p.marginType,
|
|
59
61
|
liquidationPrice: p.liquidationPrice || null
|
|
@@ -214,6 +214,8 @@ class BinanceAccountTracker extends BaseAccountTracker {
|
|
|
214
214
|
size: amt,
|
|
215
215
|
entryPrice: risk.entryPrice ?? "0",
|
|
216
216
|
unrealizedPnl: risk.unRealizedProfit ?? "0",
|
|
217
|
+
// Mapped nowhere on Binance yet — see `mapPositions`.
|
|
218
|
+
marginUsed: null,
|
|
217
219
|
leverage: risk.leverage ?? "0",
|
|
218
220
|
marginType: risk.marginType,
|
|
219
221
|
liquidationPrice: risk.liquidationPrice || null
|
|
@@ -71,6 +71,8 @@ function mapSinglePosition(p, size) {
|
|
|
71
71
|
size: size === "0" || size === "" ? "0" : side === "short" ? `-${size}` : size,
|
|
72
72
|
entryPrice: p.avgPrice ?? "0",
|
|
73
73
|
unrealizedPnl: p.unrealisedPnl ?? "0",
|
|
74
|
+
// Bybit states `positionIM` per position; mapping it is left to its own change.
|
|
75
|
+
marginUsed: null,
|
|
74
76
|
leverage: p.leverage ?? "0",
|
|
75
77
|
marginType: p.tradeMode === "1" ? "isolated" : "cross",
|
|
76
78
|
liquidationPrice: p.liqPrice || null
|
|
@@ -46,6 +46,18 @@ const HyperliquidStandardBalance = AccountBalanceCore.extend({
|
|
|
46
46
|
unrealizedPnl: SignedDecimalString,
|
|
47
47
|
maintenanceMargin: DecimalString,
|
|
48
48
|
/**
|
|
49
|
+
* Margin held across every open position, both margin modes together.
|
|
50
|
+
*
|
|
51
|
+
* Read from the positions, never derived: `positionValue / leverage` reproduces a cross
|
|
52
|
+
* position's margin exactly, and misses an isolated one — measured across 683 live positions, all
|
|
53
|
+
* 662 cross matched and all 21 isolated drifted, because an isolated leg can be topped up and
|
|
54
|
+
* accrues its own PnL. The venue states each position's figure outright, so this sums those.
|
|
55
|
+
*
|
|
56
|
+
* Adds across collateral tokens, so an account holding a non-USDC HIP-3 dex reads a total in no
|
|
57
|
+
* single currency — the same limit `accountValue` and `unrealizedPnl` carry on this variant.
|
|
58
|
+
*/
|
|
59
|
+
marginUsed: DecimalString,
|
|
60
|
+
/**
|
|
49
61
|
* The cross pool's own utilisation: Σ `crossMaintenanceMarginUsed` over Σ `crossMarginSummary
|
|
50
62
|
* .accountValue`, aggregated across every dex before the single division.
|
|
51
63
|
*
|
|
@@ -90,6 +102,8 @@ const HyperliquidUnifiedBalance = AccountBalanceCore.extend({
|
|
|
90
102
|
unrealizedPnl: SignedDecimalString,
|
|
91
103
|
/** Cross maintenance only — isolated legs carry their own margin. */
|
|
92
104
|
perpsMaintenanceMargin: DecimalString,
|
|
105
|
+
/** Margin held across every open position, both modes — read per position, never derived. */
|
|
106
|
+
marginUsed: DecimalString,
|
|
93
107
|
unifiedAccountRatio: DecimalString,
|
|
94
108
|
/**
|
|
95
109
|
* Cross notional over the COLLATERAL backing it — the spot holdings of the dex collateral tokens,
|
|
@@ -158,6 +172,8 @@ const HyperliquidPortfolioMarginBalance = AccountBalanceCore.extend({
|
|
|
158
172
|
portfolioValue: DecimalString.nullable(),
|
|
159
173
|
unrealizedPnl: SignedDecimalString,
|
|
160
174
|
perpsMaintenanceMargin: DecimalString,
|
|
175
|
+
/** Margin held across every open position, both modes — read per position, never derived. */
|
|
176
|
+
marginUsed: DecimalString,
|
|
161
177
|
/**
|
|
162
178
|
* The venue's own `spotClearinghouseState.portfolioMarginRatio`, clipped at `1.0000` by the
|
|
163
179
|
* venue itself. Null when the wire states none.
|
|
@@ -29,12 +29,14 @@ function aggregateClearinghouseStates(states) {
|
|
|
29
29
|
position.position.marginUsed
|
|
30
30
|
];
|
|
31
31
|
})).toFixed();
|
|
32
|
+
const marginUsed = sum(positions.map((position) => position.position.marginUsed)).toFixed();
|
|
32
33
|
summary.accountValue = add(summary.accountValue, state.marginSummary.accountValue);
|
|
33
34
|
summary.crossAccountValue = add(summary.crossAccountValue, state.crossMarginSummary.accountValue);
|
|
34
35
|
summary.crossNotional = add(summary.crossNotional, state.crossMarginSummary.totalNtlPos);
|
|
35
36
|
summary.crossMaintenanceMarginUsed = add(summary.crossMaintenanceMarginUsed, state.crossMaintenanceMarginUsed);
|
|
36
37
|
summary.withdrawable = add(summary.withdrawable, state.withdrawable);
|
|
37
38
|
summary.totalUnrealizedPnl = add(summary.totalUnrealizedPnl, unrealizedPnl);
|
|
39
|
+
summary.totalMarginUsed = add(summary.totalMarginUsed, marginUsed);
|
|
38
40
|
assetPositions.push(...positions);
|
|
39
41
|
summary.perDexState.push({
|
|
40
42
|
dex: dexState.dex,
|
|
@@ -77,6 +79,7 @@ function emptyClearinghouseSummary() {
|
|
|
77
79
|
crossMaintenanceMarginUsed: "0",
|
|
78
80
|
withdrawable: "0",
|
|
79
81
|
totalUnrealizedPnl: "0",
|
|
82
|
+
totalMarginUsed: "0",
|
|
80
83
|
byCollateralToken: /* @__PURE__ */ new Map(),
|
|
81
84
|
perDexState: []
|
|
82
85
|
};
|
|
@@ -115,6 +118,8 @@ function mapPositions(raw) {
|
|
|
115
118
|
size: size.toFixed(),
|
|
116
119
|
entryPrice: position.entryPx || "0",
|
|
117
120
|
unrealizedPnl: position.unrealizedPnl || "0",
|
|
121
|
+
// Read, never derived: an isolated leg can be topped up, so its margin outruns value/leverage.
|
|
122
|
+
marginUsed: position.marginUsed || null,
|
|
118
123
|
leverage: position.leverage.value.toString(),
|
|
119
124
|
marginType: position.leverage.type,
|
|
120
125
|
liquidationPrice: position.liquidationPx || null
|
|
@@ -157,6 +162,8 @@ function mapStandardBalance(context) {
|
|
|
157
162
|
unrealizedPnl: signed(context.summary.totalUnrealizedPnl),
|
|
158
163
|
// Cross-only: isolated legs carry their own margin and liquidate independently.
|
|
159
164
|
maintenanceMargin: context.summary.crossMaintenanceMarginUsed,
|
|
165
|
+
// Both modes, unlike the maintenance figure above: every position's margin is margin committed.
|
|
166
|
+
marginUsed: context.summary.totalMarginUsed,
|
|
160
167
|
// Risk levers against ACCOUNT VALUE, not the cash above: one pool, one quotient, one division.
|
|
161
168
|
crossMarginRatio: ratio(context.summary.crossMaintenanceMarginUsed, context.summary.crossAccountValue),
|
|
162
169
|
crossAccountLeverage: accountLeverage(context.summary.crossNotional, context.summary.crossAccountValue),
|
|
@@ -189,6 +196,8 @@ function mapUnifiedBalance(context) {
|
|
|
189
196
|
unrealizedPnl: signed(context.summary.totalUnrealizedPnl),
|
|
190
197
|
// Cross-only: adding isolated maintenance overstated the figure by 2x against the venue's UI.
|
|
191
198
|
perpsMaintenanceMargin: context.summary.crossMaintenanceMarginUsed,
|
|
199
|
+
// Both modes, unlike the maintenance figure above: every position's margin is margin committed.
|
|
200
|
+
marginUsed: context.summary.totalMarginUsed,
|
|
192
201
|
unifiedAccountRatio: crossRatioAgainstFreeSpot(context),
|
|
193
202
|
unifiedAccountLeverage: unifiedAccountLeverage(context),
|
|
194
203
|
perDexState: context.summary.perDexState,
|
|
@@ -218,6 +227,8 @@ function mapPortfolioMarginBalance(context) {
|
|
|
218
227
|
portfolioValue: tradingEquity,
|
|
219
228
|
unrealizedPnl: signed(context.summary.totalUnrealizedPnl),
|
|
220
229
|
perpsMaintenanceMargin: context.summary.crossMaintenanceMarginUsed,
|
|
230
|
+
// Both modes, unlike the maintenance figure above: every position's margin is margin committed.
|
|
231
|
+
marginUsed: context.summary.totalMarginUsed,
|
|
221
232
|
portfolioMarginRatio: wireMarginRatio(context),
|
|
222
233
|
portfolioAccountLeverage: portfolioAccountLeverage(context),
|
|
223
234
|
borrowCapUsed: worstBorrowCapUsed(context),
|
|
@@ -34,6 +34,22 @@ const lighterShared = {
|
|
|
34
34
|
*/
|
|
35
35
|
unrealizedPnl: SignedDecimalString.nullable(),
|
|
36
36
|
/**
|
|
37
|
+
* Margin held across every open position, both margin modes together.
|
|
38
|
+
*
|
|
39
|
+
* Summed from the positions, because the venue publishes no one field for it: `/account` states
|
|
40
|
+
* the cross requirement alone and `user_stats` states neither. Over cross positions the sum
|
|
41
|
+
* reproduces the venue's own `cross_initial_margin_requirement` exactly — rounded half-up at six
|
|
42
|
+
* decimals, the way the venue states a requirement — and an isolated leg adds the collateral it
|
|
43
|
+
* holds on top, the same both-modes reading `maintenanceMargin` takes.
|
|
44
|
+
*
|
|
45
|
+
* Rounded once, over the exact total: the per-position figures round individually, so adding
|
|
46
|
+
* those up can miss this by a decimal's last place. This is the figure that matches the venue.
|
|
47
|
+
*
|
|
48
|
+
* Null when ANY position's own margin is unreadable, for the reason `unrealizedPnl` is: a total
|
|
49
|
+
* missing one term is not a smaller version of the answer.
|
|
50
|
+
*/
|
|
51
|
+
marginUsed: DecimalString.nullable(),
|
|
52
|
+
/**
|
|
37
53
|
* Cross exposure over cross equity.
|
|
38
54
|
*
|
|
39
55
|
* A MULTIPLE, not a ratio: `0.72` reads 0.72x, and the venue publishes it that way in both
|
|
@@ -28,6 +28,7 @@ function mapLiveBalance(stats, assets, positions, markets, timestamp) {
|
|
|
28
28
|
equity,
|
|
29
29
|
perCollateral,
|
|
30
30
|
unrealizedPnl: signed(sumUnrealizedPnl(openPositions)),
|
|
31
|
+
marginUsed: usdOrNull(asMarginRequirement(sumMarginUsed(risks))),
|
|
31
32
|
// The venue states leverage and usage outright here and REST derives them, so each pair meets
|
|
32
33
|
// at one formatter — otherwise the paths would agree only while the venue kept its precision.
|
|
33
34
|
// Usage arrives in percent points and leaves as a fraction, which is the venue's scale changed.
|
|
@@ -57,6 +58,7 @@ function mapRestBalance(account, markets, timestamp) {
|
|
|
57
58
|
equity,
|
|
58
59
|
perCollateral,
|
|
59
60
|
unrealizedPnl: signed(sumUnrealizedPnl(positions)),
|
|
61
|
+
marginUsed: usdOrNull(asMarginRequirement(sumMarginUsed(risks))),
|
|
60
62
|
crossLeverage: multipleOf(sumNotional(risks, "cross"), crossEquity),
|
|
61
63
|
crossMarginUsage: fractionOf(toDecimal(account.cross_initial_margin_requirement), crossEquity),
|
|
62
64
|
crossMarginRatio: fractionOf(crossMaintenance, crossEquity),
|
|
@@ -91,6 +93,7 @@ function mapPositions(raw, includeZeroPositions = false) {
|
|
|
91
93
|
size: size.isFinite() && !size.isZero() ? size.toFixed() : "0",
|
|
92
94
|
entryPrice: rawPosition.avg_entry_price || "0",
|
|
93
95
|
unrealizedPnl: rawPosition.unrealized_pnl || "0",
|
|
96
|
+
marginUsed: usdOrNull(asMarginRequirement(positionMarginUsed(rawPosition))),
|
|
94
97
|
leverage,
|
|
95
98
|
marginType,
|
|
96
99
|
liquidationPrice: rawPosition.liquidation_price && rawPosition.liquidation_price !== "0" ? rawPosition.liquidation_price : null
|
|
@@ -181,6 +184,7 @@ function toLighterBalance(figures) {
|
|
|
181
184
|
stakingEquity: null,
|
|
182
185
|
creditedInLit: null,
|
|
183
186
|
unrealizedPnl: figures.unrealizedPnl,
|
|
187
|
+
marginUsed: figures.marginUsed,
|
|
184
188
|
crossLeverage: figures.crossLeverage,
|
|
185
189
|
maintenanceMargin: figures.maintenanceMargin,
|
|
186
190
|
crossMarginUsage: figures.crossMarginUsage,
|
|
@@ -301,12 +305,29 @@ function toPositionRisks(positions, perpByMarketId) {
|
|
|
301
305
|
risks.push({
|
|
302
306
|
marginMode: position.margin_mode === 1 ? "isolated" : "cross",
|
|
303
307
|
notional,
|
|
308
|
+
marginUsed: positionMarginUsed(position),
|
|
304
309
|
maintenance: notional === null || maintenanceFraction === null ? null : notional.times(maintenanceFraction).div(MARGIN_FRACTION_SCALE)
|
|
305
310
|
});
|
|
306
311
|
}
|
|
307
312
|
return risks;
|
|
308
313
|
}
|
|
309
314
|
__name(toPositionRisks, "toPositionRisks");
|
|
315
|
+
function positionMarginUsed(position) {
|
|
316
|
+
if (position.margin_mode === 1) {
|
|
317
|
+
return toDecimal(position.allocated_margin);
|
|
318
|
+
}
|
|
319
|
+
const positionValue = toDecimal(position.position_value);
|
|
320
|
+
const initialMarginFraction = toDecimal(position.initial_margin_fraction);
|
|
321
|
+
if (positionValue === null || initialMarginFraction === null || initialMarginFraction.lte(0)) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
return positionValue.abs().times(initialMarginFraction).div(PERCENT_SCALE);
|
|
325
|
+
}
|
|
326
|
+
__name(positionMarginUsed, "positionMarginUsed");
|
|
327
|
+
function sumMarginUsed(risks) {
|
|
328
|
+
return sumOf(risks.map((risk) => risk.marginUsed));
|
|
329
|
+
}
|
|
330
|
+
__name(sumMarginUsed, "sumMarginUsed");
|
|
310
331
|
function sumMaintenance(risks, marginMode) {
|
|
311
332
|
return sumOf(risks.filter((risk) => risk.marginMode === marginMode).map((risk) => risk.maintenance));
|
|
312
333
|
}
|
|
@@ -46,11 +46,13 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
46
46
|
startInFlight = null;
|
|
47
47
|
lifecycleVersion = 0;
|
|
48
48
|
wsConnectionVersion = 0;
|
|
49
|
-
|
|
49
|
+
// Bumped only where position state actually moves, so an in-flight REST read is invalidated by
|
|
50
|
+
// a frame that carries positions and never by one that carries the account figures alone.
|
|
51
|
+
positionStateVersion = 0;
|
|
50
52
|
assetsByAssetId = /* @__PURE__ */ new Map();
|
|
51
|
-
// Written
|
|
52
|
-
//
|
|
53
|
-
|
|
53
|
+
// Written by `account_all` and by the REST poll, whichever read the account last. The live
|
|
54
|
+
// balance takes its unrealized PnL and its maintenance margin from here.
|
|
55
|
+
positionsByMarketId = /* @__PURE__ */ new Map();
|
|
54
56
|
marketsByMarketId = /* @__PURE__ */ new Map();
|
|
55
57
|
marketsBySymbol = /* @__PURE__ */ new Map();
|
|
56
58
|
// Seeded empty, which still prices the quote asset — spot dollars are dollars before any
|
|
@@ -222,9 +224,9 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
222
224
|
handleWsOpen() {
|
|
223
225
|
const wsConnectionVersion = ++this.wsConnectionVersion;
|
|
224
226
|
this.clearSubscribeRetry();
|
|
225
|
-
this.
|
|
227
|
+
this.positionStateVersion += 1;
|
|
226
228
|
this.assetsByAssetId.clear();
|
|
227
|
-
this.
|
|
229
|
+
this.positionsByMarketId.clear();
|
|
228
230
|
this.stats = null;
|
|
229
231
|
this.hasAssets = false;
|
|
230
232
|
this.resetAccountState();
|
|
@@ -240,10 +242,10 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
240
242
|
/**
|
|
241
243
|
* Subscribes this socket epoch's account channels, and keeps trying until one attempt lands.
|
|
242
244
|
*
|
|
243
|
-
* This is the sole subscriber of `account_all` and `user_stats`, and
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
245
|
+
* This is the sole subscriber of `account_all` and `user_stats`, and no balance exists without
|
|
246
|
+
* both — see `buildBalance`. A mint that failed once and was never retried therefore left the
|
|
247
|
+
* tracker connected but permanently unsubscribed: the 2s REST poll kept positions moving while
|
|
248
|
+
* the balance stayed null until some incidental reconnect.
|
|
247
249
|
*
|
|
248
250
|
* The wait doubles up to a ceiling, and only a new connection epoch or teardown ends it.
|
|
249
251
|
*/
|
|
@@ -303,7 +305,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
307
|
onAccountAll(data) {
|
|
306
|
-
this.
|
|
308
|
+
this.positionStateVersion += 1;
|
|
307
309
|
const update = {};
|
|
308
310
|
const isSnapshot = data.type === "subscribed/account_all";
|
|
309
311
|
if (data.assets) {
|
|
@@ -320,20 +322,20 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
320
322
|
...data.positions
|
|
321
323
|
};
|
|
322
324
|
if (isSnapshot) {
|
|
323
|
-
this.
|
|
325
|
+
this.positionsByMarketId.clear();
|
|
324
326
|
}
|
|
325
327
|
for (const [marketId, position] of Object.entries(data.positions)) {
|
|
326
328
|
if (isZeroSize(position.position)) {
|
|
327
|
-
const previousPosition = this.
|
|
329
|
+
const previousPosition = this.positionsByMarketId.get(marketId);
|
|
328
330
|
if (previousPosition) {
|
|
329
331
|
positionsForMapping[marketId] = {
|
|
330
332
|
...position,
|
|
331
333
|
sign: previousPosition.sign
|
|
332
334
|
};
|
|
333
335
|
}
|
|
334
|
-
this.
|
|
336
|
+
this.positionsByMarketId.delete(marketId);
|
|
335
337
|
} else {
|
|
336
|
-
this.
|
|
338
|
+
this.positionsByMarketId.set(marketId, position);
|
|
337
339
|
}
|
|
338
340
|
}
|
|
339
341
|
const { positions, trackedAssets } = mapPositions(positionsForMapping, !isSnapshot);
|
|
@@ -347,7 +349,6 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
347
349
|
}
|
|
348
350
|
/** Keeps both published scopes: cross drives every "Cross" field, total drives the account ones. */
|
|
349
351
|
onUserStats(data) {
|
|
350
|
-
this.stateVersion += 1;
|
|
351
352
|
const stats = data.stats;
|
|
352
353
|
this.stats = {
|
|
353
354
|
accountType: toLighterAccountType(stats.account_trading_mode),
|
|
@@ -360,20 +361,22 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
360
361
|
});
|
|
361
362
|
}
|
|
362
363
|
/**
|
|
363
|
-
* The account's balance,
|
|
364
|
+
* The account's balance, from the freshest state each of its parts has.
|
|
364
365
|
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
366
|
+
* The two account scopes come from `user_stats` and the assets from `account_all`, both
|
|
367
|
+
* websocket-only. The positions come from whichever source read the account last — a frame, or
|
|
368
|
+
* the REST poll that keeps them moving while the socket says nothing (see
|
|
369
|
+
* `performPositionPoll`). The market context is venue-wide metadata on its own timer.
|
|
370
|
+
*
|
|
371
|
+
* This is the ONLY producer of the value `getBalance()` returns while the tracker is running;
|
|
372
|
+
* `snapshot()` is the separate one-shot path and never writes here.
|
|
370
373
|
*
|
|
371
374
|
* Null until both channels have delivered a frame, because a balance built from one of them
|
|
372
375
|
* would be missing half the account.
|
|
373
376
|
*/
|
|
374
377
|
buildBalance() {
|
|
375
378
|
if (!this.stats || !this.hasAssets) return null;
|
|
376
|
-
return mapLiveBalance(this.stats, this.assetsByAssetId.values(), this.
|
|
379
|
+
return mapLiveBalance(this.stats, this.assetsByAssetId.values(), this.positionsByMarketId.values(), this.marketContext(), Date.now());
|
|
377
380
|
}
|
|
378
381
|
/** The market data both read paths join account state against, as the latest read left it. */
|
|
379
382
|
marketContext() {
|
|
@@ -419,8 +422,8 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
419
422
|
pollPositions() {
|
|
420
423
|
if (this.positionPollInFlight) return this.positionPollInFlight;
|
|
421
424
|
const lifecycleVersion = this.lifecycleVersion;
|
|
422
|
-
const
|
|
423
|
-
const poll = this.performPositionPoll(lifecycleVersion,
|
|
425
|
+
const positionStateVersion = this.positionStateVersion;
|
|
426
|
+
const poll = this.performPositionPoll(lifecycleVersion, positionStateVersion).finally(() => {
|
|
424
427
|
if (this.positionPollInFlight === poll) {
|
|
425
428
|
this.positionPollInFlight = null;
|
|
426
429
|
}
|
|
@@ -429,28 +432,52 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
429
432
|
return poll;
|
|
430
433
|
}
|
|
431
434
|
/**
|
|
432
|
-
* Republishes positions
|
|
435
|
+
* Republishes positions, per-market settings, and the balance those positions move.
|
|
436
|
+
*
|
|
437
|
+
* This is what keeps the account's open result alive. Lighter republishes `account_all` when the
|
|
438
|
+
* account itself changes and not when the mark moves, so between fills the position rows hold
|
|
439
|
+
* whatever the last frame left them at. A balance built on those rows stated a dead unrealized
|
|
440
|
+
* PnL and a dead maintenance margin beside the live `user_stats` figures — and restated them on
|
|
441
|
+
* every frame under a current timestamp, so nothing marked them as old. The venue's own account
|
|
442
|
+
* read is the only source that carries the new figures, so the poll adopts it wholesale.
|
|
443
|
+
*
|
|
444
|
+
* The rows land in the same store `account_all` writes, which is what keeps `getPositions()` and
|
|
445
|
+
* the balance's `unrealizedPnl` stating one account rather than two.
|
|
433
446
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
447
|
+
* A read is dropped whole when a frame moved the position state while it was in flight: that
|
|
448
|
+
* frame is the newer account. `user_stats` deliberately does not invalidate one — it carries the
|
|
449
|
+
* account scopes and no positions at all, and a venue publishing it every tick would otherwise
|
|
450
|
+
* cancel every poll and freeze the very figures this read exists to move.
|
|
438
451
|
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
452
|
+
* No balance leaves here while the socket is down. The account scopes and the assets are dropped
|
|
453
|
+
* on the next open rather than on the drop, so a balance published mid-outage would pair these
|
|
454
|
+
* fresh positions with figures frozen at the drop — worst of it a cross margin ratio measured
|
|
455
|
+
* against a portfolio value the account no longer has. Positions still publish: they are whole.
|
|
442
456
|
*/
|
|
443
|
-
async performPositionPoll(lifecycleVersion,
|
|
457
|
+
async performPositionPoll(lifecycleVersion, positionStateVersion) {
|
|
444
458
|
try {
|
|
445
459
|
const account = await this.fetchPrimaryAccount();
|
|
446
460
|
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
447
|
-
if (!account ||
|
|
448
|
-
const
|
|
449
|
-
this.
|
|
461
|
+
if (!account || positionStateVersion !== this.positionStateVersion) return;
|
|
462
|
+
const rawPositions = account.positions ?? [];
|
|
463
|
+
this.positionsByMarketId.clear();
|
|
464
|
+
for (const position of rawPositions) {
|
|
465
|
+
if (isZeroSize(position.position)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
this.positionsByMarketId.set(String(position.market_id), position);
|
|
469
|
+
}
|
|
470
|
+
const { positions, trackedAssets } = mapPositions(toPosRecord(rawPositions), true);
|
|
471
|
+
const update = {
|
|
450
472
|
positions,
|
|
451
473
|
positionsMode: "replace",
|
|
452
474
|
trackedAssets
|
|
453
|
-
}
|
|
475
|
+
};
|
|
476
|
+
const balance = this.isConnected ? this.buildBalance() : null;
|
|
477
|
+
if (balance) {
|
|
478
|
+
update.balance = balance;
|
|
479
|
+
}
|
|
480
|
+
this.onStateUpdate(update);
|
|
454
481
|
} catch (err) {
|
|
455
482
|
if (lifecycleVersion === this.lifecycleVersion) {
|
|
456
483
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -88,6 +88,8 @@ function mapSinglePosition(p) {
|
|
|
88
88
|
size,
|
|
89
89
|
entryPrice: p.avgPx ?? "0",
|
|
90
90
|
unrealizedPnl: p.upl ?? "0",
|
|
91
|
+
// OKX states `imr` per position; mapping it is left to its own change.
|
|
92
|
+
marginUsed: null,
|
|
91
93
|
leverage: p.lever ?? "0",
|
|
92
94
|
marginType,
|
|
93
95
|
liquidationPrice: p.liqPx || null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/exchanges-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Pear Protocol Exchanges SDK",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@pear-protocol/core-sdk": "^1.
|
|
31
|
-
"@pear-protocol/types": "^1.
|
|
32
|
-
"@pear-protocol/utils": "^0.8.
|
|
30
|
+
"@pear-protocol/core-sdk": "^1.20.0",
|
|
31
|
+
"@pear-protocol/types": "^1.32.0",
|
|
32
|
+
"@pear-protocol/utils": "^0.8.1",
|
|
33
33
|
"bignumber.js": "^11.1.1",
|
|
34
34
|
"partysocket": "^1.0.3",
|
|
35
35
|
"viem": "^2.48.8",
|