@pear-protocol/exchanges-sdk 0.3.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 +182 -38
- 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 +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 +3 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
+
import { BybitAccountTracker } from './tracker';
|
|
3
|
+
|
|
4
|
+
const { fetchWalletBalanceMock, fetchPositionsMock, fetchAccountInfoMock, fetchPositionLeverageMock } = vi.hoisted(
|
|
5
|
+
() => ({
|
|
6
|
+
fetchWalletBalanceMock: vi.fn(),
|
|
7
|
+
fetchPositionsMock: vi.fn(),
|
|
8
|
+
fetchAccountInfoMock: vi.fn(),
|
|
9
|
+
fetchPositionLeverageMock: vi.fn()
|
|
10
|
+
})
|
|
11
|
+
);
|
|
12
|
+
vi.mock("../../client/bybit/rest", () => ({
|
|
13
|
+
fetchWalletBalance: fetchWalletBalanceMock,
|
|
14
|
+
fetchPositions: fetchPositionsMock,
|
|
15
|
+
fetchAccountInfo: fetchAccountInfoMock,
|
|
16
|
+
fetchPositionLeverage: fetchPositionLeverageMock
|
|
17
|
+
}));
|
|
18
|
+
const creds = { api_key: "k", api_secret: "s" };
|
|
19
|
+
function buildTracker() {
|
|
20
|
+
return new BybitAccountTracker({ demo: true, credentials: creds });
|
|
21
|
+
}
|
|
22
|
+
describe("BybitAccountTracker.snapshot", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
fetchWalletBalanceMock.mockReset();
|
|
25
|
+
fetchPositionsMock.mockReset();
|
|
26
|
+
fetchAccountInfoMock.mockReset();
|
|
27
|
+
});
|
|
28
|
+
it("returns mapped balance + positions across USDT and USDC settle coins with no socket opened", async () => {
|
|
29
|
+
fetchWalletBalanceMock.mockResolvedValue({
|
|
30
|
+
totalEquity: "2000",
|
|
31
|
+
totalWalletBalance: "2000",
|
|
32
|
+
totalMarginBalance: "2000",
|
|
33
|
+
totalAvailableBalance: "1800",
|
|
34
|
+
totalPerpUPL: "50",
|
|
35
|
+
totalInitialMargin: "100",
|
|
36
|
+
totalMaintenanceMargin: "20",
|
|
37
|
+
accountMMRate: "0.0100",
|
|
38
|
+
coin: [
|
|
39
|
+
{
|
|
40
|
+
coin: "USDT",
|
|
41
|
+
equity: "2000",
|
|
42
|
+
usdValue: "2000",
|
|
43
|
+
walletBalance: "2000",
|
|
44
|
+
unrealisedPnl: "50",
|
|
45
|
+
totalPositionIM: "100",
|
|
46
|
+
totalPositionMM: "20",
|
|
47
|
+
totalOrderIM: "0",
|
|
48
|
+
locked: "0",
|
|
49
|
+
borrowAmount: "0",
|
|
50
|
+
marginCollateral: true,
|
|
51
|
+
collateralSwitch: true
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
fetchPositionsMock.mockImplementation(async (_creds, settleCoin) => {
|
|
56
|
+
if (settleCoin === "USDT") {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
symbol: "BTCUSDT",
|
|
60
|
+
side: "Buy",
|
|
61
|
+
size: "1",
|
|
62
|
+
avgPrice: "50000",
|
|
63
|
+
unrealisedPnl: "10",
|
|
64
|
+
leverage: "5",
|
|
65
|
+
tradeMode: "0",
|
|
66
|
+
liqPrice: "40000"
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
});
|
|
72
|
+
fetchAccountInfoMock.mockResolvedValue({ marginMode: "REGULAR_MARGIN", updatedTime: "0" });
|
|
73
|
+
const snapshot = await buildTracker().snapshot();
|
|
74
|
+
expect(snapshot.balance).toMatchObject({
|
|
75
|
+
accountType: "REGULAR_MARGIN",
|
|
76
|
+
totalEquity: "2000",
|
|
77
|
+
availableToTrade: "1800",
|
|
78
|
+
maintenanceMargin: "20",
|
|
79
|
+
marginRatio: "0.0100"
|
|
80
|
+
});
|
|
81
|
+
expect(snapshot.positions).toHaveLength(1);
|
|
82
|
+
expect(snapshot.positions[0]).toMatchObject({
|
|
83
|
+
symbol: "BTCUSDT",
|
|
84
|
+
side: "long",
|
|
85
|
+
leverage: "5",
|
|
86
|
+
marginType: "cross",
|
|
87
|
+
liquidationPrice: "40000"
|
|
88
|
+
});
|
|
89
|
+
expect(fetchPositionsMock).toHaveBeenCalledWith(creds, "USDT", true);
|
|
90
|
+
expect(fetchPositionsMock).toHaveBeenCalledWith(creds, "USDC", true);
|
|
91
|
+
expect(fetchPositionLeverageMock).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
it("returns a null balance when the wallet fetch is empty", async () => {
|
|
94
|
+
fetchWalletBalanceMock.mockResolvedValue(void 0);
|
|
95
|
+
fetchPositionsMock.mockResolvedValue([]);
|
|
96
|
+
fetchAccountInfoMock.mockResolvedValue({ marginMode: "REGULAR_MARGIN", updatedTime: "0" });
|
|
97
|
+
const snapshot = await buildTracker().snapshot();
|
|
98
|
+
expect(snapshot.balance).toBeNull();
|
|
99
|
+
expect(snapshot.positions).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("BybitAccountTracker.snapshotAssets", () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
fetchPositionsMock.mockReset();
|
|
105
|
+
fetchPositionLeverageMock.mockReset();
|
|
106
|
+
});
|
|
107
|
+
it(">2 symbols: resolves positioned symbols from the two settleCoin reads and fills in the rest per symbol", async () => {
|
|
108
|
+
fetchPositionsMock.mockImplementation(
|
|
109
|
+
async (_creds, settleCoin) => settleCoin === "USDT" ? [
|
|
110
|
+
{ symbol: "BTCUSDT", size: "1", leverage: "10", tradeMode: "0" },
|
|
111
|
+
{ symbol: "ETHUSDT", size: "2", leverage: "5", tradeMode: "1" }
|
|
112
|
+
] : []
|
|
113
|
+
);
|
|
114
|
+
fetchPositionLeverageMock.mockResolvedValue({ symbol: "DOGEUSDT", size: "0", leverage: "3", tradeMode: "0" });
|
|
115
|
+
const result = await buildTracker().snapshotAssets(["BTCUSDT", "ETHUSDT", "DOGEUSDT"]);
|
|
116
|
+
expect(fetchPositionsMock).toHaveBeenCalledTimes(2);
|
|
117
|
+
expect(fetchPositionLeverageMock).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(fetchPositionLeverageMock).toHaveBeenCalledWith(creds, "DOGEUSDT", true);
|
|
119
|
+
expect(result).toEqual({
|
|
120
|
+
BTCUSDT: { coin: "BTCUSDT", leverage: "10", marginType: "cross" },
|
|
121
|
+
ETHUSDT: { coin: "ETHUSDT", leverage: "5", marginType: "isolated" },
|
|
122
|
+
DOGEUSDT: { coin: "DOGEUSDT", leverage: "3", marginType: "cross" }
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
it("\u22642 symbols: skips the settleCoin batch and reads per symbol directly", async () => {
|
|
126
|
+
fetchPositionLeverageMock.mockResolvedValue({ symbol: "BTCUSDT", size: "0", leverage: "10", tradeMode: "0" });
|
|
127
|
+
const result = await buildTracker().snapshotAssets(["BTCUSDT"]);
|
|
128
|
+
expect(fetchPositionsMock).not.toHaveBeenCalled();
|
|
129
|
+
expect(fetchPositionLeverageMock).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(result.BTCUSDT).toEqual({ coin: "BTCUSDT", leverage: "10", marginType: "cross" });
|
|
131
|
+
});
|
|
132
|
+
it("maps a failed fill-in to null while the others succeed", async () => {
|
|
133
|
+
fetchPositionLeverageMock.mockImplementation(async (_creds, symbol) => {
|
|
134
|
+
if (symbol === "NOPEUSDT") throw new Error("10001");
|
|
135
|
+
return { symbol, size: "0", leverage: "10", tradeMode: "0" };
|
|
136
|
+
});
|
|
137
|
+
const result = await buildTracker().snapshotAssets(["BTCUSDT", "NOPEUSDT"]);
|
|
138
|
+
expect(result.BTCUSDT).toMatchObject({ leverage: "10", marginType: "cross" });
|
|
139
|
+
expect(result.NOPEUSDT).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
it("throws when every per-symbol read fails", async () => {
|
|
142
|
+
fetchPositionLeverageMock.mockRejectedValue(new Error("down"));
|
|
143
|
+
await expect(buildTracker().snapshotAssets(["BTCUSDT", "ETHUSDT"])).rejects.toThrow("every position read failed");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -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 };
|