@pear-protocol/exchanges-sdk 0.3.0 → 0.5.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/binance/rest.js +4 -3
- package/dist/client/bybit/rest.js +2 -1
- package/dist/client/hyperliquid/client.test.js +41 -0
- package/dist/client/hyperliquid/exchange-ws.js +3 -2
- package/dist/client/hyperliquid/rest.js +28 -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 +110 -37
- package/dist/client/lighter/rest.test.js +152 -0
- package/dist/client/lighter/ws.js +7 -2
- package/dist/client/okx/rest.js +20 -10
- 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 +189 -38
- package/dist/sdk.js +22 -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 +0 -2
- package/dist/syncer/base.test.js +0 -1
- package/dist/syncer/hyperliquid/syncer.js +96 -43
- 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 +30 -6
- package/dist/syncer/lighter/syncer.test.js +132 -6
- 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,36 +1,53 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import { positionKey } from '../utils';
|
|
3
3
|
import { QUOTE_ASSET } from './const';
|
|
4
|
-
import { LighterAccountType } from './types';
|
|
4
|
+
import { LighterAccountType, LighterAccountTier } from './types';
|
|
5
5
|
|
|
6
|
-
function
|
|
6
|
+
function buildAssetBalances(rawAssets) {
|
|
7
7
|
const free = {};
|
|
8
8
|
const used = {};
|
|
9
9
|
const total = {};
|
|
10
10
|
const assets = [];
|
|
11
|
-
for (const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
for (const rawAsset of rawAssets) {
|
|
12
|
+
const balance = new BigNumber(rawAsset.balance || "0");
|
|
13
|
+
const lockedBalance = new BigNumber(rawAsset.locked_balance || "0");
|
|
14
|
+
const marginBalance = new BigNumber(rawAsset.margin_balance || "0");
|
|
15
|
+
const totalAmount = balance.plus(marginBalance).toFixed();
|
|
16
|
+
const freeAmount = BigNumber.max(balance.plus(marginBalance).minus(lockedBalance), 0).toFixed();
|
|
17
|
+
free[rawAsset.symbol] = freeAmount;
|
|
18
|
+
used[rawAsset.symbol] = rawAsset.locked_balance;
|
|
19
|
+
total[rawAsset.symbol] = totalAmount;
|
|
18
20
|
assets.push({
|
|
19
|
-
asset:
|
|
20
|
-
free:
|
|
21
|
-
used:
|
|
22
|
-
total:
|
|
23
|
-
walletBalance:
|
|
24
|
-
usdValue:
|
|
21
|
+
asset: rawAsset.symbol,
|
|
22
|
+
free: freeAmount,
|
|
23
|
+
used: rawAsset.locked_balance,
|
|
24
|
+
total: totalAmount,
|
|
25
|
+
walletBalance: totalAmount,
|
|
26
|
+
usdValue: rawAsset.symbol === QUOTE_ASSET ? totalAmount : "0"
|
|
25
27
|
});
|
|
26
28
|
}
|
|
29
|
+
return { free, used, total, assets };
|
|
30
|
+
}
|
|
31
|
+
function mapBalance(summary, assetsByAssetId, rawPositions, marketsByMarketId) {
|
|
32
|
+
const { free, used, total, assets } = buildAssetBalances(assetsByAssetId.values());
|
|
27
33
|
const portfolioValueBN = new BigNumber(summary.portfolioValue);
|
|
28
34
|
const marginUsageFrac = new BigNumber(summary.marginUsagePct).div(100);
|
|
29
35
|
const unrealizedPnl = portfolioValueBN.minus(summary.collateral).toFixed(6);
|
|
30
36
|
const initialMarginUsed = marginUsageFrac.multipliedBy(portfolioValueBN).toFixed(6);
|
|
31
|
-
const
|
|
37
|
+
const positionRisks = calculatePositionRisks(rawPositions, marketsByMarketId);
|
|
38
|
+
const maintenanceMargin = positionRisks.reduce(
|
|
39
|
+
(totalMaintenance, positionRisk) => totalMaintenance.plus(positionRisk.maintenance),
|
|
40
|
+
new BigNumber(0)
|
|
41
|
+
);
|
|
42
|
+
const isolatedEquity = positionRisks.reduce(
|
|
43
|
+
(totalEquity, positionRisk) => positionRisk.marginMode === "isolated" ? totalEquity.plus(positionRisk.equity) : totalEquity,
|
|
44
|
+
new BigNumber(0)
|
|
45
|
+
);
|
|
46
|
+
const crossEquity = new BigNumber(summary.crossPortfolioValue ?? portfolioValueBN.minus(isolatedEquity));
|
|
47
|
+
const marginRatio = maximumMarginRatio(positionRisks, crossEquity);
|
|
32
48
|
return {
|
|
33
|
-
accountType:
|
|
49
|
+
accountType: summary.accountType,
|
|
50
|
+
accountTier: summary.accountTier,
|
|
34
51
|
free,
|
|
35
52
|
used,
|
|
36
53
|
total,
|
|
@@ -39,32 +56,79 @@ function mapBalance(summary, assetsByAssetId) {
|
|
|
39
56
|
walletBalance: summary.collateral,
|
|
40
57
|
unrealizedPnl,
|
|
41
58
|
initialMarginUsed,
|
|
42
|
-
maintenanceMargin:
|
|
59
|
+
maintenanceMargin: maintenanceMargin.toFixed(6),
|
|
43
60
|
marginRatio,
|
|
44
61
|
assets,
|
|
45
62
|
timestamp: Date.now()
|
|
46
63
|
};
|
|
47
64
|
}
|
|
48
|
-
function
|
|
65
|
+
function buildAccountBalance(account, marketsByMarketId, accountTier) {
|
|
66
|
+
const { free, used, total, assets } = buildAssetBalances(account.assets ?? []);
|
|
67
|
+
const positions = account.positions ?? [];
|
|
68
|
+
const allocatedMargin = sumPositionField(positions, "allocated_margin", (position) => position.margin_mode === 1);
|
|
69
|
+
const totalCollateral = new BigNumber(account.collateral).plus(allocatedMargin);
|
|
70
|
+
const isolatedInitialMargin = positions.reduce((totalInitialMargin, position) => {
|
|
71
|
+
if (position.margin_mode !== 1 || new BigNumber(position.position).isZero()) {
|
|
72
|
+
return totalInitialMargin;
|
|
73
|
+
}
|
|
74
|
+
return totalInitialMargin.plus(
|
|
75
|
+
new BigNumber(position.position_value).abs().times(position.initial_margin_fraction).div(100)
|
|
76
|
+
);
|
|
77
|
+
}, new BigNumber(0));
|
|
78
|
+
const initialMargin = new BigNumber(account.cross_initial_margin_requirement).plus(isolatedInitialMargin);
|
|
79
|
+
const positionRisks = calculatePositionRisks(positions, marketsByMarketId);
|
|
80
|
+
const isolatedMaintenanceMargin = positionRisks.reduce(
|
|
81
|
+
(totalMaintenance, positionRisk) => positionRisk.marginMode === "isolated" ? totalMaintenance.plus(positionRisk.maintenance) : totalMaintenance,
|
|
82
|
+
new BigNumber(0)
|
|
83
|
+
);
|
|
84
|
+
const maintenanceMarginBN = new BigNumber(account.cross_maintenance_margin_requirement).plus(
|
|
85
|
+
isolatedMaintenanceMargin
|
|
86
|
+
);
|
|
87
|
+
const marginRatio = maximumMarginRatio(
|
|
88
|
+
positionRisks.filter((positionRisk) => positionRisk.marginMode === "isolated"),
|
|
89
|
+
new BigNumber(account.cross_asset_value),
|
|
90
|
+
new BigNumber(account.cross_maintenance_margin_requirement)
|
|
91
|
+
);
|
|
92
|
+
const unrealizedPnl = sumPositionField(positions, "unrealized_pnl").toFixed(6);
|
|
93
|
+
return {
|
|
94
|
+
accountType: toLighterAccountType(account.account_trading_mode),
|
|
95
|
+
accountTier,
|
|
96
|
+
free,
|
|
97
|
+
used,
|
|
98
|
+
total,
|
|
99
|
+
availableToTrade: account.available_balance,
|
|
100
|
+
totalEquity: account.total_asset_value,
|
|
101
|
+
walletBalance: totalCollateral.toFixed(6),
|
|
102
|
+
unrealizedPnl,
|
|
103
|
+
initialMarginUsed: initialMargin.toFixed(6),
|
|
104
|
+
maintenanceMargin: maintenanceMarginBN.toFixed(6),
|
|
105
|
+
marginRatio,
|
|
106
|
+
assets,
|
|
107
|
+
timestamp: Date.now()
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function mapPositions(raw, includeZeroPositions = false) {
|
|
49
111
|
const positions = /* @__PURE__ */ new Map();
|
|
50
112
|
const trackedAssets = /* @__PURE__ */ new Map();
|
|
51
|
-
for (const
|
|
52
|
-
const
|
|
53
|
-
const side =
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
113
|
+
for (const rawPosition of Object.values(raw)) {
|
|
114
|
+
const unsignedSize = new BigNumber(rawPosition.position || "0");
|
|
115
|
+
const side = rawPosition.sign === -1 ? "short" : "long";
|
|
116
|
+
const size = rawPosition.sign === -1 ? unsignedSize.negated() : unsignedSize;
|
|
117
|
+
const leverage = leverageFromImf(rawPosition.initial_margin_fraction);
|
|
118
|
+
const marginType = rawPosition.margin_mode === 1 ? "isolated" : "cross";
|
|
119
|
+
const marketId = String(rawPosition.market_id);
|
|
120
|
+
trackedAssets.set(marketId, { coin: rawPosition.symbol, leverage, marginType });
|
|
121
|
+
if (!includeZeroPositions && unsignedSize.isZero()) continue;
|
|
122
|
+
positions.set(positionKey(rawPosition.symbol, side), {
|
|
123
|
+
symbol: rawPosition.symbol,
|
|
59
124
|
side,
|
|
60
|
-
size:
|
|
61
|
-
entryPrice:
|
|
62
|
-
unrealizedPnl:
|
|
125
|
+
size: size.isFinite() && !size.isZero() ? size.toFixed() : "0",
|
|
126
|
+
entryPrice: rawPosition.avg_entry_price || "0",
|
|
127
|
+
unrealizedPnl: rawPosition.unrealized_pnl || "0",
|
|
63
128
|
leverage,
|
|
64
129
|
marginType,
|
|
65
|
-
liquidationPrice:
|
|
130
|
+
liquidationPrice: rawPosition.liquidation_price && rawPosition.liquidation_price !== "0" ? rawPosition.liquidation_price : null
|
|
66
131
|
});
|
|
67
|
-
trackedAssets.set(marketId, { coin: p.symbol, leverage, marginType });
|
|
68
132
|
}
|
|
69
133
|
return { positions, trackedAssets };
|
|
70
134
|
}
|
|
@@ -73,5 +137,71 @@ function leverageFromImf(imfPct) {
|
|
|
73
137
|
if (!pct.isFinite() || pct.lte(0)) return "1";
|
|
74
138
|
return new BigNumber(100).div(pct).integerValue(BigNumber.ROUND_FLOOR).toFixed();
|
|
75
139
|
}
|
|
140
|
+
function mapMarketDefaultAssetInfo(market) {
|
|
141
|
+
return {
|
|
142
|
+
coin: market.symbol,
|
|
143
|
+
leverage: leverageFromImf(new BigNumber(market.default_initial_margin_fraction).div(100).toFixed()),
|
|
144
|
+
marginType: "cross"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function toLighterAccountType(accountTradingMode) {
|
|
148
|
+
return accountTradingMode === 1 ? LighterAccountType.UNIFIED : LighterAccountType.CLASSIC;
|
|
149
|
+
}
|
|
150
|
+
function toLighterAccountTier(userTier) {
|
|
151
|
+
switch (userTier?.trim().toLowerCase()) {
|
|
152
|
+
case "std":
|
|
153
|
+
case "standard":
|
|
154
|
+
return LighterAccountTier.STANDARD;
|
|
155
|
+
case "premium":
|
|
156
|
+
return LighterAccountTier.PREMIUM;
|
|
157
|
+
case "plus":
|
|
158
|
+
return LighterAccountTier.PLUS;
|
|
159
|
+
default:
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function calculatePositionRisks(rawPositions, marketsByMarketId) {
|
|
164
|
+
const positionRisks = [];
|
|
165
|
+
for (const rawPosition of rawPositions) {
|
|
166
|
+
const market = marketsByMarketId.get(rawPosition.market_id);
|
|
167
|
+
if (!market || new BigNumber(rawPosition.position).isZero()) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const marginMode = rawPosition.margin_mode === 1 ? "isolated" : "cross";
|
|
171
|
+
const maintenance = new BigNumber(rawPosition.position_value).abs().times(market.maintenance_margin_fraction).div(1e4);
|
|
172
|
+
const equity = marginMode === "isolated" ? new BigNumber(rawPosition.allocated_margin).plus(rawPosition.unrealized_pnl) : new BigNumber(0);
|
|
173
|
+
positionRisks.push({ marginMode, maintenance, equity });
|
|
174
|
+
}
|
|
175
|
+
return positionRisks;
|
|
176
|
+
}
|
|
177
|
+
function maximumMarginRatio(positionRisks, crossEquity, authoritativeCrossMaintenance) {
|
|
178
|
+
const calculatedCrossMaintenance = positionRisks.reduce(
|
|
179
|
+
(totalMaintenance, positionRisk) => positionRisk.marginMode === "cross" ? totalMaintenance.plus(positionRisk.maintenance) : totalMaintenance,
|
|
180
|
+
new BigNumber(0)
|
|
181
|
+
);
|
|
182
|
+
const crossMaintenance = authoritativeCrossMaintenance ?? calculatedCrossMaintenance;
|
|
183
|
+
let maximumRatio = maintenanceRatio(crossMaintenance, crossEquity);
|
|
184
|
+
for (const positionRisk of positionRisks) {
|
|
185
|
+
if (positionRisk.marginMode === "isolated") {
|
|
186
|
+
maximumRatio = BigNumber.max(maximumRatio, maintenanceRatio(positionRisk.maintenance, positionRisk.equity));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return maximumRatio.toFixed(4);
|
|
190
|
+
}
|
|
191
|
+
function maintenanceRatio(maintenance, equity) {
|
|
192
|
+
if (equity.gt(0)) {
|
|
193
|
+
return maintenance.div(equity);
|
|
194
|
+
}
|
|
195
|
+
return maintenance.gt(0) ? new BigNumber(1) : new BigNumber(0);
|
|
196
|
+
}
|
|
197
|
+
function sumPositionField(positions, field, include = () => true) {
|
|
198
|
+
let total = new BigNumber(0);
|
|
199
|
+
for (const position of positions) {
|
|
200
|
+
if (include(position)) {
|
|
201
|
+
total = total.plus(position[field] || "0");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return total;
|
|
205
|
+
}
|
|
76
206
|
|
|
77
|
-
export { leverageFromImf, mapBalance, mapPositions };
|
|
207
|
+
export { buildAccountBalance, leverageFromImf, mapBalance, mapMarketDefaultAssetInfo, mapPositions, toLighterAccountTier, toLighterAccountType };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildAccountBalance, mapBalance, toLighterAccountTier, mapPositions } from './mapper';
|
|
3
|
+
import { LighterAccountTier, LighterAccountType } from './types';
|
|
4
|
+
|
|
5
|
+
const markets = /* @__PURE__ */ new Map([
|
|
6
|
+
[
|
|
7
|
+
1,
|
|
8
|
+
{
|
|
9
|
+
market_id: 1,
|
|
10
|
+
symbol: "BTC",
|
|
11
|
+
status: "active",
|
|
12
|
+
default_initial_margin_fraction: 1e3,
|
|
13
|
+
maintenance_margin_fraction: 1e3
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
2,
|
|
18
|
+
{
|
|
19
|
+
market_id: 2,
|
|
20
|
+
symbol: "ETH",
|
|
21
|
+
status: "active",
|
|
22
|
+
default_initial_margin_fraction: 2e3,
|
|
23
|
+
maintenance_margin_fraction: 500
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
]);
|
|
27
|
+
describe("Lighter account mapping", () => {
|
|
28
|
+
it("combines cross and isolated collateral, risk requirements, and unrealized PnL", () => {
|
|
29
|
+
const balance = buildAccountBalance(
|
|
30
|
+
{
|
|
31
|
+
account_type: 2,
|
|
32
|
+
account_trading_mode: 1,
|
|
33
|
+
collateral: "1000",
|
|
34
|
+
total_asset_value: "1145",
|
|
35
|
+
available_balance: "800",
|
|
36
|
+
cross_asset_value: "1020",
|
|
37
|
+
cross_initial_margin_requirement: "100",
|
|
38
|
+
cross_maintenance_margin_requirement: "50",
|
|
39
|
+
assets: [
|
|
40
|
+
{
|
|
41
|
+
symbol: "USDC",
|
|
42
|
+
balance: "10",
|
|
43
|
+
locked_balance: "2",
|
|
44
|
+
margin_balance: "1000"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
positions: [
|
|
48
|
+
{
|
|
49
|
+
market_id: 1,
|
|
50
|
+
symbol: "BTC",
|
|
51
|
+
initial_margin_fraction: "10",
|
|
52
|
+
sign: 1,
|
|
53
|
+
position: "1",
|
|
54
|
+
avg_entry_price: "1000",
|
|
55
|
+
position_value: "1000",
|
|
56
|
+
unrealized_pnl: "20",
|
|
57
|
+
liquidation_price: "500",
|
|
58
|
+
margin_mode: 0,
|
|
59
|
+
allocated_margin: "0"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
market_id: 2,
|
|
63
|
+
symbol: "ETH",
|
|
64
|
+
initial_margin_fraction: "20",
|
|
65
|
+
sign: -1,
|
|
66
|
+
position: "2",
|
|
67
|
+
avg_entry_price: "250",
|
|
68
|
+
position_value: "500",
|
|
69
|
+
unrealized_pnl: "-25",
|
|
70
|
+
liquidation_price: "400",
|
|
71
|
+
margin_mode: 1,
|
|
72
|
+
allocated_margin: "150"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
markets,
|
|
77
|
+
LighterAccountTier.PLUS
|
|
78
|
+
);
|
|
79
|
+
expect(balance).toMatchObject({
|
|
80
|
+
accountType: "unified",
|
|
81
|
+
accountTier: "plus",
|
|
82
|
+
totalEquity: "1145",
|
|
83
|
+
walletBalance: "1150.000000",
|
|
84
|
+
unrealizedPnl: "-5.000000",
|
|
85
|
+
initialMarginUsed: "200.000000",
|
|
86
|
+
maintenanceMargin: "75.000000",
|
|
87
|
+
marginRatio: "0.2000",
|
|
88
|
+
free: { USDC: "1008" },
|
|
89
|
+
used: { USDC: "2" },
|
|
90
|
+
total: { USDC: "1010" }
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
it("uses the riskiest independently liquidatable bucket for live margin ratio", () => {
|
|
94
|
+
const balance = mapBalance(
|
|
95
|
+
{
|
|
96
|
+
collateral: "1050",
|
|
97
|
+
portfolioValue: "950",
|
|
98
|
+
crossPortfolioValue: "900",
|
|
99
|
+
availableToTrade: "800",
|
|
100
|
+
marginUsagePct: "0",
|
|
101
|
+
accountType: LighterAccountType.CLASSIC
|
|
102
|
+
},
|
|
103
|
+
/* @__PURE__ */ new Map(),
|
|
104
|
+
[
|
|
105
|
+
rawPosition({ position_value: "100", margin_mode: 0 }),
|
|
106
|
+
rawPosition({
|
|
107
|
+
market_id: 2,
|
|
108
|
+
symbol: "ETH",
|
|
109
|
+
position_value: "500",
|
|
110
|
+
unrealized_pnl: "-100",
|
|
111
|
+
margin_mode: 1,
|
|
112
|
+
allocated_margin: "150"
|
|
113
|
+
})
|
|
114
|
+
],
|
|
115
|
+
markets
|
|
116
|
+
);
|
|
117
|
+
expect(balance.maintenanceMargin).toBe("35.000000");
|
|
118
|
+
expect(balance.marginRatio).toBe("0.5000");
|
|
119
|
+
});
|
|
120
|
+
it.each(["0", "-1"])("fails closed for live maintenance risk with nonpositive equity %s", (portfolioValue) => {
|
|
121
|
+
const balance = mapBalance(
|
|
122
|
+
{
|
|
123
|
+
collateral: "0",
|
|
124
|
+
portfolioValue,
|
|
125
|
+
availableToTrade: "0",
|
|
126
|
+
marginUsagePct: "0",
|
|
127
|
+
accountType: LighterAccountType.CLASSIC
|
|
128
|
+
},
|
|
129
|
+
/* @__PURE__ */ new Map(),
|
|
130
|
+
[rawPosition({ position_value: "1000" })],
|
|
131
|
+
markets
|
|
132
|
+
);
|
|
133
|
+
expect(balance.maintenanceMargin).toBe("100.000000");
|
|
134
|
+
expect(balance.marginRatio).toBe("1.0000");
|
|
135
|
+
});
|
|
136
|
+
it.each(["0", "-1"])("fails closed for REST maintenance risk with nonpositive equity %s", (totalAssetValue) => {
|
|
137
|
+
const balance = buildAccountBalance(
|
|
138
|
+
{
|
|
139
|
+
account_type: 0,
|
|
140
|
+
account_trading_mode: 0,
|
|
141
|
+
collateral: "0",
|
|
142
|
+
total_asset_value: totalAssetValue,
|
|
143
|
+
available_balance: "0",
|
|
144
|
+
cross_asset_value: totalAssetValue,
|
|
145
|
+
cross_initial_margin_requirement: "0",
|
|
146
|
+
cross_maintenance_margin_requirement: "10"
|
|
147
|
+
},
|
|
148
|
+
markets
|
|
149
|
+
);
|
|
150
|
+
expect(balance.maintenanceMargin).toBe("10.000000");
|
|
151
|
+
expect(balance.marginRatio).toBe("1.0000");
|
|
152
|
+
});
|
|
153
|
+
it("maps authenticated account-limit tier names and never infers a tier from account_type", () => {
|
|
154
|
+
expect(toLighterAccountTier("std")).toBe(LighterAccountTier.STANDARD);
|
|
155
|
+
expect(toLighterAccountTier("STANDARD")).toBe(LighterAccountTier.STANDARD);
|
|
156
|
+
expect(toLighterAccountTier("premium")).toBe(LighterAccountTier.PREMIUM);
|
|
157
|
+
expect(toLighterAccountTier("plus")).toBe(LighterAccountTier.PLUS);
|
|
158
|
+
expect(toLighterAccountTier("unknown")).toBeUndefined();
|
|
159
|
+
const balance = buildAccountBalance(
|
|
160
|
+
{
|
|
161
|
+
account_type: 2,
|
|
162
|
+
account_trading_mode: 0,
|
|
163
|
+
collateral: "0",
|
|
164
|
+
total_asset_value: "0",
|
|
165
|
+
available_balance: "0",
|
|
166
|
+
cross_asset_value: "0",
|
|
167
|
+
cross_initial_margin_requirement: "0",
|
|
168
|
+
cross_maintenance_margin_requirement: "0"
|
|
169
|
+
},
|
|
170
|
+
/* @__PURE__ */ new Map()
|
|
171
|
+
);
|
|
172
|
+
expect(balance.accountTier).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
it("filters full-snapshot zero rows and preserves signed short exposure", () => {
|
|
175
|
+
const positions = mapPositions({
|
|
176
|
+
1: rawPosition({ market_id: 1, symbol: "BTC", sign: -1, position: "2" }),
|
|
177
|
+
2: rawPosition({ market_id: 2, symbol: "ETH", sign: 1, position: "0.0000" })
|
|
178
|
+
}).positions;
|
|
179
|
+
expect(Array.from(positions.values())).toEqual([
|
|
180
|
+
expect.objectContaining({ symbol: "BTC", side: "short", size: "-2" })
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
function rawPosition(overrides) {
|
|
185
|
+
return {
|
|
186
|
+
market_id: 1,
|
|
187
|
+
symbol: "BTC",
|
|
188
|
+
initial_margin_fraction: "10",
|
|
189
|
+
sign: 1,
|
|
190
|
+
position: "1",
|
|
191
|
+
avg_entry_price: "1000",
|
|
192
|
+
position_value: "1000",
|
|
193
|
+
unrealized_pnl: "0",
|
|
194
|
+
liquidation_price: "500",
|
|
195
|
+
margin_mode: 0,
|
|
196
|
+
allocated_margin: "0",
|
|
197
|
+
...overrides
|
|
198
|
+
};
|
|
199
|
+
}
|