@pear-protocol/exchanges-sdk 0.2.0 → 0.4.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 +78 -12
- package/dist/client/hyperliquid/client.test.js +41 -0
- package/dist/client/hyperliquid/exchange-ws.js +3 -2
- package/dist/client/hyperliquid/rest.js +26 -5
- package/dist/client/hyperliquid/ws.js +16 -4
- package/dist/client/index.js +2 -2
- package/dist/client/lighter/constants.js +9 -1
- package/dist/client/lighter/constants.test.js +22 -0
- package/dist/client/lighter/exchange-ws.js +2 -2
- package/dist/client/lighter/rest.js +101 -32
- package/dist/client/lighter/rest.test.js +152 -0
- package/dist/client/lighter/ws.js +7 -2
- package/dist/client/okx/rest.js +18 -9
- package/dist/helper/factory.js +2 -2
- package/dist/helper/hyperliquid/helper.js +28 -13
- package/dist/helper/hyperliquid/helper.test.js +45 -0
- package/dist/helper/lighter/helper.js +77 -16
- package/dist/helper/lighter/helper.test.js +289 -0
- package/dist/helper/lighter/types.js +9 -8
- package/dist/index.d.ts +239 -47
- package/dist/sdk.js +20 -16
- package/dist/sdk.test.js +101 -0
- package/dist/status/hyperliquid/status.js +18 -5
- package/dist/status/hyperliquid/status.test.js +81 -0
- package/dist/status/lighter/status.js +18 -11
- package/dist/status/lighter/status.test.js +75 -0
- package/dist/syncer/base.js +107 -25
- package/dist/syncer/base.test.js +131 -0
- package/dist/syncer/binance/syncer.js +48 -20
- package/dist/syncer/bybit/syncer.js +39 -15
- package/dist/syncer/hyperliquid/funding.js +24 -0
- package/dist/syncer/hyperliquid/funding.test.js +63 -0
- package/dist/syncer/hyperliquid/syncer.js +183 -24
- package/dist/syncer/hyperliquid/syncer.test.js +274 -0
- package/dist/syncer/lighter/funding.js +25 -0
- package/dist/syncer/lighter/funding.test.js +48 -0
- package/dist/syncer/lighter/syncer.js +93 -41
- package/dist/syncer/lighter/syncer.test.js +132 -6
- package/dist/syncer/okx/syncer.js +41 -17
- package/dist/tracker/base.js +23 -6
- package/dist/tracker/base.test.js +132 -0
- package/dist/tracker/binance/tracker.js +48 -18
- package/dist/tracker/binance/tracker.test.js +138 -0
- package/dist/tracker/bybit/tracker.js +78 -23
- package/dist/tracker/bybit/tracker.test.js +145 -0
- package/dist/tracker/hyperliquid/mapper.js +244 -44
- package/dist/tracker/hyperliquid/mapper.test.js +255 -0
- package/dist/tracker/hyperliquid/tracker.js +241 -74
- package/dist/tracker/hyperliquid/tracker.test.js +587 -0
- package/dist/tracker/index.js +15 -37
- package/dist/tracker/lighter/mapper.js +163 -33
- package/dist/tracker/lighter/mapper.test.js +199 -0
- package/dist/tracker/lighter/tracker.js +219 -45
- package/dist/tracker/lighter/tracker.test.js +515 -0
- package/dist/tracker/lighter/types.js +10 -2
- package/dist/tracker/okx/tracker.js +50 -16
- package/dist/tracker/okx/tracker.test.js +126 -0
- package/dist/tracker/types.js +2 -1
- package/dist/tracker/utils.test.js +8 -0
- package/package.json +6 -5
|
@@ -1,35 +1,141 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import { positionKey } from '../utils';
|
|
3
|
-
import { COLLATERAL_TOKEN_MAP } from './types';
|
|
3
|
+
import { COLLATERAL_TOKEN_MAP, HyperliquidAccountType } from './types';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const LEGACY_COLLATERAL_TOKEN_MAP = new Map(
|
|
6
|
+
Object.entries(COLLATERAL_TOKEN_MAP).map(([token, coin]) => [Number(token), coin])
|
|
7
|
+
);
|
|
8
|
+
const FULL_UTILIZATION_RATIO = "1.0000";
|
|
9
|
+
const ZERO_COLLATERAL_SUMMARY = {
|
|
10
|
+
accountValue: "0",
|
|
11
|
+
totalMarginUsed: "0",
|
|
12
|
+
crossMaintenanceMarginUsed: "0",
|
|
13
|
+
isolatedMaintenanceMarginUsed: "0",
|
|
14
|
+
isolatedMarginUsed: "0",
|
|
15
|
+
withdrawable: "0",
|
|
16
|
+
totalUnrealizedPnl: "0"
|
|
17
|
+
};
|
|
18
|
+
function aggregateClearinghouseStates(states) {
|
|
19
|
+
const summary = {
|
|
20
|
+
accountValue: "0",
|
|
21
|
+
totalMarginUsed: "0",
|
|
22
|
+
totalRawUsd: "0",
|
|
23
|
+
crossMaintenanceMarginUsed: "0",
|
|
24
|
+
isolatedMaintenanceMarginUsed: "0",
|
|
25
|
+
maximumStandardMarginRatio: "0.0000",
|
|
26
|
+
withdrawable: "0",
|
|
27
|
+
totalUnrealizedPnl: "0",
|
|
28
|
+
byCollateralToken: /* @__PURE__ */ new Map()
|
|
29
|
+
};
|
|
30
|
+
const assetPositions = [];
|
|
31
|
+
for (const dexState of states) {
|
|
32
|
+
const state = dexState.state;
|
|
33
|
+
const positions = state.assetPositions ?? [];
|
|
34
|
+
const unrealizedPnl = sum(positions.map((position) => position.position.unrealizedPnl));
|
|
35
|
+
const isolatedRisks = positions.flatMap((position) => {
|
|
36
|
+
if (position.position.leverage.type !== "isolated") {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return [{ collateral: position.position.marginUsed, maintenance: isolatedMaintenanceMargin(dexState, position) }];
|
|
40
|
+
});
|
|
41
|
+
const isolatedMaintenanceMarginUsed = sum(isolatedRisks.map((risk) => risk.maintenance));
|
|
42
|
+
const isolatedMarginUsed = sum(isolatedRisks.map((risk) => risk.collateral));
|
|
43
|
+
let dexMaximumMarginRatio = ratio(state.crossMaintenanceMarginUsed, state.crossMarginSummary.accountValue);
|
|
44
|
+
for (const isolatedRisk of isolatedRisks) {
|
|
45
|
+
dexMaximumMarginRatio = BigNumber.max(
|
|
46
|
+
dexMaximumMarginRatio,
|
|
47
|
+
ratio(isolatedRisk.maintenance, isolatedRisk.collateral)
|
|
48
|
+
).toFixed(4);
|
|
49
|
+
}
|
|
50
|
+
summary.accountValue = add(summary.accountValue, state.marginSummary.accountValue);
|
|
51
|
+
summary.totalMarginUsed = add(summary.totalMarginUsed, state.marginSummary.totalMarginUsed);
|
|
52
|
+
summary.totalRawUsd = add(summary.totalRawUsd, state.marginSummary.totalRawUsd);
|
|
53
|
+
summary.crossMaintenanceMarginUsed = add(summary.crossMaintenanceMarginUsed, state.crossMaintenanceMarginUsed);
|
|
54
|
+
summary.isolatedMaintenanceMarginUsed = add(summary.isolatedMaintenanceMarginUsed, isolatedMaintenanceMarginUsed);
|
|
55
|
+
summary.maximumStandardMarginRatio = BigNumber.max(
|
|
56
|
+
summary.maximumStandardMarginRatio,
|
|
57
|
+
dexMaximumMarginRatio
|
|
58
|
+
).toFixed(4);
|
|
59
|
+
summary.withdrawable = add(summary.withdrawable, state.withdrawable);
|
|
60
|
+
summary.totalUnrealizedPnl = add(summary.totalUnrealizedPnl, unrealizedPnl);
|
|
61
|
+
assetPositions.push(...positions);
|
|
62
|
+
if (dexState.collateralToken === null) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const collateral = summary.byCollateralToken.get(dexState.collateralToken) ?? ZERO_COLLATERAL_SUMMARY;
|
|
66
|
+
summary.byCollateralToken.set(dexState.collateralToken, {
|
|
67
|
+
accountValue: add(collateral.accountValue, state.marginSummary.accountValue),
|
|
68
|
+
totalMarginUsed: add(collateral.totalMarginUsed, state.marginSummary.totalMarginUsed),
|
|
69
|
+
crossMaintenanceMarginUsed: add(collateral.crossMaintenanceMarginUsed, state.crossMaintenanceMarginUsed),
|
|
70
|
+
isolatedMaintenanceMarginUsed: add(collateral.isolatedMaintenanceMarginUsed, isolatedMaintenanceMarginUsed),
|
|
71
|
+
isolatedMarginUsed: add(collateral.isolatedMarginUsed, isolatedMarginUsed),
|
|
72
|
+
withdrawable: add(collateral.withdrawable, state.withdrawable),
|
|
73
|
+
totalUnrealizedPnl: add(collateral.totalUnrealizedPnl, unrealizedPnl)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { summary, assetPositions };
|
|
77
|
+
}
|
|
78
|
+
function mapBalance(summary, spotCoinBalances, accountType, collateralTokenMap = LEGACY_COLLATERAL_TOKEN_MAP) {
|
|
79
|
+
if (!accountType) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
switch (accountType) {
|
|
83
|
+
case HyperliquidAccountType.DEFAULT:
|
|
84
|
+
case HyperliquidAccountType.DISABLED:
|
|
85
|
+
return mapStandardBalance(summary, accountType, collateralTokenMap);
|
|
86
|
+
case HyperliquidAccountType.UNIFIED_ACCOUNT:
|
|
87
|
+
return mapUnifiedBalance(summary, spotCoinBalances, accountType, collateralTokenMap);
|
|
88
|
+
case HyperliquidAccountType.DEX_ABSTRACTION:
|
|
89
|
+
case HyperliquidAccountType.PORTFOLIO_MARGIN:
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function mapPositions(raw) {
|
|
94
|
+
const positions = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const assetPosition of raw) {
|
|
96
|
+
const position = assetPosition.position;
|
|
97
|
+
const size = new BigNumber(position.szi || "0");
|
|
98
|
+
if (size.isZero()) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const coin = position.coin || "";
|
|
102
|
+
const side = size.isPositive() ? "long" : "short";
|
|
103
|
+
positions.set(positionKey(coin, side), {
|
|
104
|
+
symbol: coin,
|
|
105
|
+
side,
|
|
106
|
+
size: size.toFixed(),
|
|
107
|
+
entryPrice: position.entryPx || "0",
|
|
108
|
+
unrealizedPnl: position.unrealizedPnl || "0",
|
|
109
|
+
leverage: position.leverage.value.toString(),
|
|
110
|
+
marginType: position.leverage.type,
|
|
111
|
+
liquidationPrice: position.liquidationPx || null
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return positions;
|
|
115
|
+
}
|
|
116
|
+
function mapStandardBalance(summary, accountType, collateralTokenMap) {
|
|
8
117
|
const free = {};
|
|
9
118
|
const used = {};
|
|
10
119
|
const total = {};
|
|
11
120
|
const assets = [];
|
|
12
|
-
for (const [,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
total[coin.coin] = coinTotal;
|
|
121
|
+
for (const [token, collateral] of summary.byCollateralToken) {
|
|
122
|
+
const coin = collateralTokenMap.get(token);
|
|
123
|
+
if (!coin) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
free[coin] = collateral.withdrawable;
|
|
127
|
+
used[coin] = collateral.totalMarginUsed;
|
|
128
|
+
total[coin] = collateral.accountValue;
|
|
21
129
|
assets.push({
|
|
22
|
-
asset: coin
|
|
23
|
-
free:
|
|
24
|
-
used:
|
|
25
|
-
total:
|
|
26
|
-
walletBalance:
|
|
27
|
-
usdValue:
|
|
130
|
+
asset: coin,
|
|
131
|
+
free: collateral.withdrawable,
|
|
132
|
+
used: collateral.totalMarginUsed,
|
|
133
|
+
total: collateral.accountValue,
|
|
134
|
+
walletBalance: subtract(collateral.accountValue, collateral.totalUnrealizedPnl),
|
|
135
|
+
usdValue: collateral.accountValue
|
|
28
136
|
});
|
|
29
137
|
}
|
|
30
|
-
const
|
|
31
|
-
const walletBalance = accountValueBN.minus(summary.totalUnrealizedPnl).toFixed();
|
|
32
|
-
const marginRatio = accountValueBN.gt(0) ? new BigNumber(summary.crossMaintenanceMarginUsed).div(accountValueBN).toFixed(4) : "0.0000";
|
|
138
|
+
const maintenanceMargin = add(summary.crossMaintenanceMarginUsed, summary.isolatedMaintenanceMarginUsed);
|
|
33
139
|
return {
|
|
34
140
|
accountType,
|
|
35
141
|
free,
|
|
@@ -37,35 +143,129 @@ function mapBalance(summary, spotCoinBalances, accountType, availableToTradeByCo
|
|
|
37
143
|
total,
|
|
38
144
|
availableToTrade: summary.withdrawable,
|
|
39
145
|
totalEquity: summary.accountValue,
|
|
40
|
-
walletBalance,
|
|
146
|
+
walletBalance: subtract(summary.accountValue, summary.totalUnrealizedPnl),
|
|
41
147
|
unrealizedPnl: summary.totalUnrealizedPnl,
|
|
42
148
|
initialMarginUsed: summary.totalMarginUsed,
|
|
43
|
-
maintenanceMargin
|
|
44
|
-
marginRatio,
|
|
149
|
+
maintenanceMargin,
|
|
150
|
+
marginRatio: summary.maximumStandardMarginRatio,
|
|
45
151
|
assets,
|
|
46
152
|
timestamp: Date.now()
|
|
47
153
|
};
|
|
48
154
|
}
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
155
|
+
function mapUnifiedBalance(summary, spotCoinBalances, accountType, collateralTokenMap) {
|
|
156
|
+
const free = {};
|
|
157
|
+
const used = {};
|
|
158
|
+
const total = {};
|
|
159
|
+
const assets = [];
|
|
160
|
+
let totalEquity = new BigNumber(0);
|
|
161
|
+
let availableToTrade = new BigNumber(0);
|
|
162
|
+
for (const balance of spotCoinBalances.values()) {
|
|
163
|
+
if (!collateralTokenMap.has(balance.token)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const collateral = summary.byCollateralToken.get(balance.token) ?? ZERO_COLLATERAL_SUMMARY;
|
|
167
|
+
const coinUsed = balance.hold;
|
|
168
|
+
const coinFree = subtract(balance.total, coinUsed);
|
|
169
|
+
const coinWalletBalance = subtract(balance.total, collateral.totalUnrealizedPnl);
|
|
170
|
+
free[balance.coin] = coinFree;
|
|
171
|
+
used[balance.coin] = coinUsed;
|
|
172
|
+
total[balance.coin] = balance.total;
|
|
173
|
+
totalEquity = totalEquity.plus(balance.total);
|
|
174
|
+
availableToTrade = availableToTrade.plus(balance.availableAfterMaintenance ?? "0");
|
|
175
|
+
assets.push({
|
|
176
|
+
asset: balance.coin,
|
|
177
|
+
free: coinFree,
|
|
178
|
+
used: coinUsed,
|
|
179
|
+
total: balance.total,
|
|
180
|
+
walletBalance: coinWalletBalance,
|
|
181
|
+
usdValue: balance.total
|
|
66
182
|
});
|
|
67
183
|
}
|
|
68
|
-
|
|
184
|
+
const maintenanceMargin = add(summary.crossMaintenanceMarginUsed, summary.isolatedMaintenanceMarginUsed);
|
|
185
|
+
const totalEquityValue = totalEquity.toFixed();
|
|
186
|
+
const walletBalance = subtract(totalEquityValue, summary.totalUnrealizedPnl);
|
|
187
|
+
return {
|
|
188
|
+
accountType,
|
|
189
|
+
free,
|
|
190
|
+
used,
|
|
191
|
+
total,
|
|
192
|
+
availableToTrade: availableToTrade.toFixed(),
|
|
193
|
+
// Unified spot totals are the authoritative equity balances and already include perp PnL.
|
|
194
|
+
totalEquity: totalEquityValue,
|
|
195
|
+
walletBalance,
|
|
196
|
+
unrealizedPnl: summary.totalUnrealizedPnl,
|
|
197
|
+
initialMarginUsed: summary.totalMarginUsed,
|
|
198
|
+
maintenanceMargin,
|
|
199
|
+
marginRatio: unifiedMarginRatio(summary, spotCoinBalances),
|
|
200
|
+
assets,
|
|
201
|
+
timestamp: Date.now()
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function unifiedMarginRatio(summary, spotCoinBalances) {
|
|
205
|
+
let maximumRatio = new BigNumber(0);
|
|
206
|
+
for (const [token, collateral] of summary.byCollateralToken) {
|
|
207
|
+
const spotBalance = [...spotCoinBalances.values()].find((balance) => balance.token === token);
|
|
208
|
+
const available = new BigNumber(spotBalance?.total ?? "0").minus(collateral.isolatedMarginUsed);
|
|
209
|
+
maximumRatio = BigNumber.max(maximumRatio, ratio(collateral.crossMaintenanceMarginUsed, available.toFixed()));
|
|
210
|
+
}
|
|
211
|
+
return maximumRatio.toFixed(4);
|
|
212
|
+
}
|
|
213
|
+
function isolatedMaintenanceMargin(dexState, assetPosition) {
|
|
214
|
+
const position = assetPosition.position;
|
|
215
|
+
if (position.leverage.type !== "isolated" || position.maxLeverage <= 0) {
|
|
216
|
+
return "0";
|
|
217
|
+
}
|
|
218
|
+
const notional = new BigNumber(position.positionValue).abs();
|
|
219
|
+
const marginTiers = findMarginTiers(dexState, position.coin);
|
|
220
|
+
const firstMarginTier = marginTiers?.[0];
|
|
221
|
+
if (!marginTiers || !firstMarginTier || marginTiers.some((tier) => tier.maxLeverage <= 0)) {
|
|
222
|
+
return notional.div(position.maxLeverage).div(2).toFixed();
|
|
223
|
+
}
|
|
224
|
+
let activeTier = firstMarginTier;
|
|
225
|
+
let maintenanceDeduction = new BigNumber(0);
|
|
226
|
+
for (const nextTier of marginTiers.slice(1)) {
|
|
227
|
+
if (notional.lt(nextTier.lowerBound)) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
const previousRate = maintenanceMarginRate(activeTier.maxLeverage);
|
|
231
|
+
const nextRate = maintenanceMarginRate(nextTier.maxLeverage);
|
|
232
|
+
maintenanceDeduction = maintenanceDeduction.plus(
|
|
233
|
+
new BigNumber(nextTier.lowerBound).times(nextRate.minus(previousRate))
|
|
234
|
+
);
|
|
235
|
+
activeTier = nextTier;
|
|
236
|
+
}
|
|
237
|
+
return notional.times(maintenanceMarginRate(activeTier.maxLeverage)).minus(maintenanceDeduction).toFixed();
|
|
238
|
+
}
|
|
239
|
+
function findMarginTiers(dexState, coin) {
|
|
240
|
+
if (!dexState.meta) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const unprefixedCoin = dexState.dex && coin.startsWith(`${dexState.dex}:`) ? coin.slice(dexState.dex.length + 1) : coin;
|
|
244
|
+
const asset = dexState.meta.universe.find(
|
|
245
|
+
(candidate) => candidate.name === coin || candidate.name === unprefixedCoin
|
|
246
|
+
);
|
|
247
|
+
const marginTable = dexState.meta.marginTables.find(([id]) => id === asset?.marginTableId)?.[1];
|
|
248
|
+
return marginTable?.marginTiers.length ? marginTable.marginTiers : null;
|
|
249
|
+
}
|
|
250
|
+
function maintenanceMarginRate(maxLeverage) {
|
|
251
|
+
return new BigNumber(1).div(maxLeverage).div(2);
|
|
252
|
+
}
|
|
253
|
+
function add(left, right) {
|
|
254
|
+
return new BigNumber(left || "0").plus(right || "0").toFixed();
|
|
255
|
+
}
|
|
256
|
+
function subtract(left, right) {
|
|
257
|
+
return new BigNumber(left || "0").minus(right || "0").toFixed();
|
|
258
|
+
}
|
|
259
|
+
function sum(values) {
|
|
260
|
+
return values.reduce((total, value) => total.plus(value || "0"), new BigNumber(0)).toFixed();
|
|
261
|
+
}
|
|
262
|
+
function ratio(numerator, denominator) {
|
|
263
|
+
const numeratorValue = new BigNumber(numerator || "0");
|
|
264
|
+
const denominatorValue = new BigNumber(denominator || "0");
|
|
265
|
+
if (denominatorValue.gt(0)) {
|
|
266
|
+
return numeratorValue.div(denominatorValue).toFixed(4);
|
|
267
|
+
}
|
|
268
|
+
return numeratorValue.gt(0) ? FULL_UTILIZATION_RATIO : "0.0000";
|
|
69
269
|
}
|
|
70
270
|
|
|
71
|
-
export { mapBalance, mapPositions };
|
|
271
|
+
export { aggregateClearinghouseStates, mapBalance, mapPositions };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { aggregateClearinghouseStates, mapBalance } from './mapper';
|
|
3
|
+
import { HyperliquidAccountType } from './types';
|
|
4
|
+
|
|
5
|
+
function buildState(overrides) {
|
|
6
|
+
const leverage = overrides.leverage === "isolated" ? { type: "isolated", value: 10, rawUsd: overrides.marginUsed ?? "0" } : { type: "cross", value: 10 };
|
|
7
|
+
return {
|
|
8
|
+
marginSummary: {
|
|
9
|
+
accountValue: overrides.accountValue,
|
|
10
|
+
totalNtlPos: "0",
|
|
11
|
+
totalRawUsd: overrides.accountValue,
|
|
12
|
+
totalMarginUsed: overrides.totalMarginUsed
|
|
13
|
+
},
|
|
14
|
+
crossMarginSummary: {
|
|
15
|
+
accountValue: overrides.crossAccountValue ?? overrides.accountValue,
|
|
16
|
+
totalNtlPos: "0",
|
|
17
|
+
totalRawUsd: overrides.crossAccountValue ?? overrides.accountValue,
|
|
18
|
+
totalMarginUsed: overrides.totalMarginUsed
|
|
19
|
+
},
|
|
20
|
+
crossMaintenanceMarginUsed: overrides.crossMaintenanceMarginUsed,
|
|
21
|
+
withdrawable: overrides.withdrawable,
|
|
22
|
+
assetPositions: [
|
|
23
|
+
{
|
|
24
|
+
type: "oneWay",
|
|
25
|
+
position: {
|
|
26
|
+
coin: overrides.coin,
|
|
27
|
+
szi: "1",
|
|
28
|
+
leverage,
|
|
29
|
+
entryPx: "1000",
|
|
30
|
+
positionValue: overrides.positionValue ?? "1000",
|
|
31
|
+
unrealizedPnl: overrides.unrealizedPnl,
|
|
32
|
+
returnOnEquity: "0.1",
|
|
33
|
+
liquidationPx: "800",
|
|
34
|
+
marginUsed: overrides.marginUsed ?? "100",
|
|
35
|
+
maxLeverage: overrides.maxLeverage ?? 10,
|
|
36
|
+
cumFunding: { allTime: "0", sinceOpen: "0", sinceChange: "0" }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
time: 0
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function dexState(state, collateralToken, dex = "", meta = null) {
|
|
44
|
+
return {
|
|
45
|
+
dex,
|
|
46
|
+
collateralToken,
|
|
47
|
+
meta,
|
|
48
|
+
state
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
describe("Hyperliquid balance mapping", () => {
|
|
52
|
+
it("uses the maximum independent cross and isolated risk ratios for Standard accounts", () => {
|
|
53
|
+
const state = buildState({
|
|
54
|
+
accountValue: "120",
|
|
55
|
+
crossAccountValue: "80",
|
|
56
|
+
totalMarginUsed: "40",
|
|
57
|
+
crossMaintenanceMarginUsed: "5",
|
|
58
|
+
withdrawable: "70",
|
|
59
|
+
coin: "BTC",
|
|
60
|
+
unrealizedPnl: "20",
|
|
61
|
+
leverage: "isolated",
|
|
62
|
+
positionValue: "1000",
|
|
63
|
+
maxLeverage: 10
|
|
64
|
+
});
|
|
65
|
+
const { summary, assetPositions } = aggregateClearinghouseStates([dexState(state, 0)]);
|
|
66
|
+
const balance = mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT);
|
|
67
|
+
expect(summary.accountValue).toBe("120");
|
|
68
|
+
expect(summary.isolatedMaintenanceMarginUsed).toBe("50");
|
|
69
|
+
expect(assetPositions).toHaveLength(1);
|
|
70
|
+
expect(balance).toMatchObject({
|
|
71
|
+
accountType: "default",
|
|
72
|
+
totalEquity: "120",
|
|
73
|
+
walletBalance: "100",
|
|
74
|
+
initialMarginUsed: "40",
|
|
75
|
+
maintenanceMargin: "55",
|
|
76
|
+
marginRatio: "0.5000",
|
|
77
|
+
availableToTrade: "70"
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it("applies tiered maintenance rates and continuity deductions to isolated positions", () => {
|
|
81
|
+
const state = buildState({
|
|
82
|
+
accountValue: "5000",
|
|
83
|
+
totalMarginUsed: "100",
|
|
84
|
+
crossMaintenanceMarginUsed: "0",
|
|
85
|
+
withdrawable: "4900",
|
|
86
|
+
coin: "BTC",
|
|
87
|
+
unrealizedPnl: "0",
|
|
88
|
+
leverage: "isolated",
|
|
89
|
+
positionValue: "2000",
|
|
90
|
+
maxLeverage: 50
|
|
91
|
+
});
|
|
92
|
+
const meta = {
|
|
93
|
+
collateralToken: 0,
|
|
94
|
+
universe: [{ name: "BTC", szDecimals: 5, maxLeverage: 50, marginTableId: 1 }],
|
|
95
|
+
marginTables: [
|
|
96
|
+
[
|
|
97
|
+
1,
|
|
98
|
+
{
|
|
99
|
+
description: "tiered",
|
|
100
|
+
marginTiers: [
|
|
101
|
+
{ lowerBound: "0", maxLeverage: 50 },
|
|
102
|
+
{ lowerBound: "1000", maxLeverage: 25 }
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
const { summary } = aggregateClearinghouseStates([dexState(state, 0, "", meta)]);
|
|
109
|
+
expect(summary.isolatedMaintenanceMarginUsed).toBe("30");
|
|
110
|
+
});
|
|
111
|
+
it("uses the maximum per-DEX Standard margin ratio instead of a blended aggregate ratio", () => {
|
|
112
|
+
const higherRiskState = buildState({
|
|
113
|
+
accountValue: "100",
|
|
114
|
+
totalMarginUsed: "20",
|
|
115
|
+
crossMaintenanceMarginUsed: "10",
|
|
116
|
+
withdrawable: "80",
|
|
117
|
+
coin: "BTC",
|
|
118
|
+
unrealizedPnl: "0"
|
|
119
|
+
});
|
|
120
|
+
const lowerRiskState = buildState({
|
|
121
|
+
accountValue: "900",
|
|
122
|
+
totalMarginUsed: "90",
|
|
123
|
+
crossMaintenanceMarginUsed: "45",
|
|
124
|
+
withdrawable: "810",
|
|
125
|
+
coin: "xyz:ETH",
|
|
126
|
+
unrealizedPnl: "0"
|
|
127
|
+
});
|
|
128
|
+
const { summary } = aggregateClearinghouseStates([
|
|
129
|
+
dexState(higherRiskState, 0),
|
|
130
|
+
dexState(lowerRiskState, 360, "xyz")
|
|
131
|
+
]);
|
|
132
|
+
const balance = mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT);
|
|
133
|
+
expect(balance).toMatchObject({
|
|
134
|
+
totalEquity: "1000",
|
|
135
|
+
maintenanceMargin: "55",
|
|
136
|
+
marginRatio: "0.1000"
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it("maps network-specific collateral token indexes through spot metadata", () => {
|
|
140
|
+
const state = buildState({
|
|
141
|
+
accountValue: "100",
|
|
142
|
+
totalMarginUsed: "10",
|
|
143
|
+
crossMaintenanceMarginUsed: "5",
|
|
144
|
+
withdrawable: "90",
|
|
145
|
+
coin: "test:ETH",
|
|
146
|
+
unrealizedPnl: "0"
|
|
147
|
+
});
|
|
148
|
+
const { summary } = aggregateClearinghouseStates([dexState(state, 1452, "test")]);
|
|
149
|
+
const balance = mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT, /* @__PURE__ */ new Map([[1452, "USDH"]]));
|
|
150
|
+
expect(balance).toMatchObject({
|
|
151
|
+
free: { USDH: "90" },
|
|
152
|
+
used: { USDH: "10" },
|
|
153
|
+
total: { USDH: "100" },
|
|
154
|
+
assets: [{ asset: "USDH" }]
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
it("reports at least full utilization when Standard maintenance is positive without collateral", () => {
|
|
158
|
+
const state = buildState({
|
|
159
|
+
accountValue: "0",
|
|
160
|
+
crossAccountValue: "0",
|
|
161
|
+
totalMarginUsed: "10",
|
|
162
|
+
crossMaintenanceMarginUsed: "5",
|
|
163
|
+
withdrawable: "0",
|
|
164
|
+
coin: "BTC",
|
|
165
|
+
unrealizedPnl: "-100"
|
|
166
|
+
});
|
|
167
|
+
const { summary } = aggregateClearinghouseStates([dexState(state, 0)]);
|
|
168
|
+
expect(mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT)?.marginRatio).toBe("1.0000");
|
|
169
|
+
});
|
|
170
|
+
it("uses authoritative spot holds and post-maintenance availability for Unified accounts", () => {
|
|
171
|
+
const usdcState = buildState({
|
|
172
|
+
accountValue: "999",
|
|
173
|
+
totalMarginUsed: "30",
|
|
174
|
+
crossMaintenanceMarginUsed: "10",
|
|
175
|
+
withdrawable: "999",
|
|
176
|
+
coin: "BTC",
|
|
177
|
+
unrealizedPnl: "5",
|
|
178
|
+
leverage: "isolated",
|
|
179
|
+
marginUsed: "20",
|
|
180
|
+
positionValue: "0"
|
|
181
|
+
});
|
|
182
|
+
const usdhState = buildState({
|
|
183
|
+
accountValue: "999",
|
|
184
|
+
totalMarginUsed: "40",
|
|
185
|
+
crossMaintenanceMarginUsed: "20",
|
|
186
|
+
withdrawable: "999",
|
|
187
|
+
coin: "xyz:ETH",
|
|
188
|
+
unrealizedPnl: "0"
|
|
189
|
+
});
|
|
190
|
+
const { summary } = aggregateClearinghouseStates([dexState(usdcState, 0), dexState(usdhState, 360, "xyz")]);
|
|
191
|
+
const spotBalances = /* @__PURE__ */ new Map([
|
|
192
|
+
["USDC", { coin: "USDC", token: 0, total: "100", hold: "5", availableAfterMaintenance: "70" }],
|
|
193
|
+
["USDH", { coin: "USDH", token: 360, total: "400", hold: "10", availableAfterMaintenance: "380" }]
|
|
194
|
+
]);
|
|
195
|
+
const balance = mapBalance(summary, spotBalances, HyperliquidAccountType.UNIFIED_ACCOUNT);
|
|
196
|
+
expect(balance).toMatchObject({
|
|
197
|
+
accountType: "unifiedAccount",
|
|
198
|
+
walletBalance: "495",
|
|
199
|
+
totalEquity: "500",
|
|
200
|
+
availableToTrade: "450",
|
|
201
|
+
marginRatio: "0.1250"
|
|
202
|
+
});
|
|
203
|
+
expect(balance?.free).toEqual({ USDC: "95", USDH: "390" });
|
|
204
|
+
expect(balance?.used).toEqual({ USDC: "5", USDH: "10" });
|
|
205
|
+
expect(balance?.assets).toEqual([
|
|
206
|
+
expect.objectContaining({ asset: "USDC", walletBalance: "95", total: "100" }),
|
|
207
|
+
expect.objectContaining({ asset: "USDH", walletBalance: "400", total: "400" })
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
it("reports at least full utilization when Unified cross maintenance has no available collateral", () => {
|
|
211
|
+
const state = buildState({
|
|
212
|
+
accountValue: "0",
|
|
213
|
+
totalMarginUsed: "10",
|
|
214
|
+
crossMaintenanceMarginUsed: "5",
|
|
215
|
+
withdrawable: "0",
|
|
216
|
+
coin: "BTC",
|
|
217
|
+
unrealizedPnl: "-100"
|
|
218
|
+
});
|
|
219
|
+
const { summary } = aggregateClearinghouseStates([dexState(state, 0)]);
|
|
220
|
+
const spotBalances = /* @__PURE__ */ new Map([["USDC", { coin: "USDC", token: 0, total: "0", hold: "0" }]]);
|
|
221
|
+
expect(mapBalance(summary, spotBalances, HyperliquidAccountType.UNIFIED_ACCOUNT)?.marginRatio).toBe("1.0000");
|
|
222
|
+
});
|
|
223
|
+
it("does not fabricate Portfolio Margin or deprecated DEX-abstraction account totals", () => {
|
|
224
|
+
const sharedState = buildState({
|
|
225
|
+
accountValue: "100",
|
|
226
|
+
totalMarginUsed: "10",
|
|
227
|
+
crossMaintenanceMarginUsed: "5",
|
|
228
|
+
withdrawable: "90",
|
|
229
|
+
coin: "BTC",
|
|
230
|
+
unrealizedPnl: "0"
|
|
231
|
+
});
|
|
232
|
+
const { summary } = aggregateClearinghouseStates([
|
|
233
|
+
dexState(sharedState, 0),
|
|
234
|
+
dexState(sharedState, 0, "builder-dex")
|
|
235
|
+
]);
|
|
236
|
+
expect(summary.accountValue).toBe("200");
|
|
237
|
+
expect(mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEX_ABSTRACTION)).toBeNull();
|
|
238
|
+
expect(mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.PORTFOLIO_MARGIN)).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
it("returns a zeroed aggregate for an empty DEX list", () => {
|
|
241
|
+
const { summary, assetPositions } = aggregateClearinghouseStates([]);
|
|
242
|
+
expect(summary).toMatchObject({
|
|
243
|
+
accountValue: "0",
|
|
244
|
+
totalMarginUsed: "0",
|
|
245
|
+
totalRawUsd: "0",
|
|
246
|
+
crossMaintenanceMarginUsed: "0",
|
|
247
|
+
isolatedMaintenanceMarginUsed: "0",
|
|
248
|
+
maximumStandardMarginRatio: "0.0000",
|
|
249
|
+
withdrawable: "0",
|
|
250
|
+
totalUnrealizedPnl: "0"
|
|
251
|
+
});
|
|
252
|
+
expect(summary.byCollateralToken.size).toBe(0);
|
|
253
|
+
expect(assetPositions).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
});
|