@pear-protocol/exchanges-sdk 0.6.6 → 0.7.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 +105 -25
- package/dist/client/bybit/rest.js +8 -1
- package/dist/client/hyperliquid/rest.js +21 -1
- package/dist/client/lighter/rest.js +6 -12
- package/dist/client/okx/rest.js +5 -1
- package/dist/helper/lighter/helper.js +6 -2
- package/dist/index.d.ts +1112 -74
- package/dist/sdk.js +9 -2
- package/dist/tracker/balance-core.js +89 -0
- package/dist/tracker/balance.js +25 -0
- package/dist/tracker/basket-size.js +194 -0
- package/dist/tracker/binance/balance.js +11 -0
- package/dist/tracker/binance/mapper.js +87 -42
- package/dist/tracker/binance/tracker.js +29 -14
- package/dist/tracker/binance/types.js +0 -6
- package/dist/tracker/bybit/balance.js +14 -0
- package/dist/tracker/bybit/mapper.js +109 -52
- package/dist/tracker/bybit/tracker.js +242 -33
- package/dist/tracker/bybit/types.js +0 -7
- package/dist/tracker/hyperliquid/account-mode.js +14 -0
- package/dist/tracker/hyperliquid/balance.js +184 -0
- package/dist/tracker/hyperliquid/basket-size.js +93 -0
- package/dist/tracker/hyperliquid/coin-name.js +18 -0
- package/dist/tracker/hyperliquid/collateral.js +48 -0
- package/dist/tracker/hyperliquid/decimal.js +28 -0
- package/dist/tracker/hyperliquid/mapper.js +399 -176
- package/dist/tracker/hyperliquid/spot-pricing.js +106 -0
- package/dist/tracker/hyperliquid/tracker.js +237 -78
- package/dist/tracker/hyperliquid/types.js +1 -7
- package/dist/tracker/index.js +9 -7
- package/dist/tracker/lighter/balance.js +87 -0
- package/dist/tracker/lighter/basket-size.js +74 -0
- package/dist/tracker/lighter/mapper.js +324 -145
- package/dist/tracker/lighter/tracker.js +228 -117
- package/dist/tracker/lighter/types.js +1 -7
- package/dist/tracker/okx/balance.js +17 -0
- package/dist/tracker/okx/mapper.js +156 -58
- package/dist/tracker/okx/tracker.js +228 -29
- package/dist/tracker/okx/types.js +0 -8
- package/dist/tracker/types.js +0 -24
- package/package.json +7 -9
package/dist/sdk.js
CHANGED
|
@@ -42,6 +42,14 @@ class ExchangesSDK {
|
|
|
42
42
|
const connection = await this.fetchCredentials(tradeAccountId);
|
|
43
43
|
return this.buildTracker(connection).snapshot();
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Sizes a basket in one shot. Routed to the tracker's REST path on purpose: this entry point
|
|
47
|
+
* builds a throwaway tracker per call, so it must open no socket.
|
|
48
|
+
*/
|
|
49
|
+
async maxBasketSize(tradeAccountId, input) {
|
|
50
|
+
const connection = await this.fetchCredentials(tradeAccountId);
|
|
51
|
+
return this.buildTracker(connection).snapshotMaxBasketSize(input);
|
|
52
|
+
}
|
|
45
53
|
async snapshotAssets(tradeAccountId, coins) {
|
|
46
54
|
const connection = await this.fetchCredentials(tradeAccountId);
|
|
47
55
|
return this.buildTracker(connection).snapshotAssets(coins);
|
|
@@ -74,8 +82,7 @@ class ExchangesSDK {
|
|
|
74
82
|
return createAccountTracker(connection.connector, {
|
|
75
83
|
demo: this.demo,
|
|
76
84
|
account: connection.account,
|
|
77
|
-
credentials: connection.credentials
|
|
78
|
-
sdk: this.sdk
|
|
85
|
+
credentials: connection.credentials
|
|
79
86
|
});
|
|
80
87
|
}
|
|
81
88
|
async fetchCredentials(tradeAccountId) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const DecimalString = z.string().regex(/^\d+(\.\d+)?$/);
|
|
4
|
+
const SignedDecimalString = z.string().regex(/^[+-]\d+(\.\d+)?$/);
|
|
5
|
+
const MarginMode = z.enum([
|
|
6
|
+
"cross",
|
|
7
|
+
"isolated"
|
|
8
|
+
]);
|
|
9
|
+
const Side = z.enum([
|
|
10
|
+
"BUY",
|
|
11
|
+
"SELL"
|
|
12
|
+
]);
|
|
13
|
+
const AccountMode = z.enum([
|
|
14
|
+
"hyperliquid:standard",
|
|
15
|
+
"hyperliquid:unified",
|
|
16
|
+
"hyperliquid:portfolioMargin",
|
|
17
|
+
"lighter:classic",
|
|
18
|
+
"lighter:unified",
|
|
19
|
+
"binance:singleAsset",
|
|
20
|
+
"binance:multiAsset",
|
|
21
|
+
"bybit:isolatedMargin",
|
|
22
|
+
"bybit:regularMargin",
|
|
23
|
+
"bybit:portfolioMargin",
|
|
24
|
+
"okx:spot",
|
|
25
|
+
"okx:futures",
|
|
26
|
+
"okx:multiCurrencyMargin",
|
|
27
|
+
"okx:portfolioMargin"
|
|
28
|
+
]);
|
|
29
|
+
const CollateralBalance = z.object({
|
|
30
|
+
asset: z.string(),
|
|
31
|
+
equity: DecimalString,
|
|
32
|
+
availableMargin: DecimalString.nullable(),
|
|
33
|
+
withdrawable: DecimalString.nullable()
|
|
34
|
+
});
|
|
35
|
+
const AccountBalanceCore = z.object({
|
|
36
|
+
/**
|
|
37
|
+
* The account's whole equity in USD, and `null` when the venue cannot state ONE spendable figure.
|
|
38
|
+
* The reasons are per-venue, and `perCollateral` is where the truth lives in every one of them:
|
|
39
|
+
*
|
|
40
|
+
* - a holding the venue cannot price — Lighter spot needs a last trade and an inactive book has
|
|
41
|
+
* none; Hyperliquid spot needs a USDC-quoted pair and the venue lists none for every token.
|
|
42
|
+
* Reporting the rest would understate the account by exactly the part we could not value.
|
|
43
|
+
* - two funded pools that cannot fund each other — `binance:singleAsset`, `okx:spot` and
|
|
44
|
+
* `okx:futures` run one collateral pool per settlement currency, so once a second pool holds
|
|
45
|
+
* value no single number is spendable across both.
|
|
46
|
+
* - a figure the venue left blank — CEX account endpoints blank their account-level fields per
|
|
47
|
+
* mode, and a blank is "not stated", never a zero.
|
|
48
|
+
*
|
|
49
|
+
* Nullable on EVERY variant on purpose: a consumer that narrows to one mode handles the same
|
|
50
|
+
* `string | null` it would have handled without narrowing, so no variant can quietly stop being
|
|
51
|
+
* safe when a venue starts blanking a field it used to fill.
|
|
52
|
+
*/
|
|
53
|
+
totalEquity: DecimalString.nullable(),
|
|
54
|
+
/** What can leave the trading account right now. */
|
|
55
|
+
withdrawableBalance: DecimalString.nullable(),
|
|
56
|
+
/**
|
|
57
|
+
* Collateral guaranteed usable as margin for EVERY asset in the tracked set — a floor, not a
|
|
58
|
+
* per-asset figure. On venues where headroom varies by asset (Hyperliquid), this is the minimum
|
|
59
|
+
* across that set, so it is only meaningful relative to the assets it was computed over.
|
|
60
|
+
*/
|
|
61
|
+
availableMargin: DecimalString.nullable(),
|
|
62
|
+
/**
|
|
63
|
+
* Whether ONE pool funds every position. `false` means the rows are separate budgets that
|
|
64
|
+
* cannot fund each other; `true` means they are holdings inside one shared pool, where only
|
|
65
|
+
* the scalars are spendable.
|
|
66
|
+
*/
|
|
67
|
+
poolsShared: z.boolean(),
|
|
68
|
+
/** Per-collateral rows in each asset's own units. Empty when the venue publishes none. */
|
|
69
|
+
perCollateral: z.array(CollateralBalance),
|
|
70
|
+
timestamp: z.number().int()
|
|
71
|
+
});
|
|
72
|
+
const ACCOUNT_MODE_LABELS = {
|
|
73
|
+
"hyperliquid:standard": "Standard",
|
|
74
|
+
"hyperliquid:unified": "Unified Account",
|
|
75
|
+
"hyperliquid:portfolioMargin": "Portfolio Margin",
|
|
76
|
+
"lighter:classic": "Classic",
|
|
77
|
+
"lighter:unified": "Unified",
|
|
78
|
+
"binance:singleAsset": "Single-Asset Mode",
|
|
79
|
+
"binance:multiAsset": "Multi-Asset Mode",
|
|
80
|
+
"bybit:isolatedMargin": "Isolated Margin",
|
|
81
|
+
"bybit:regularMargin": "Cross Margin",
|
|
82
|
+
"bybit:portfolioMargin": "Portfolio Margin",
|
|
83
|
+
"okx:spot": "Spot Mode",
|
|
84
|
+
"okx:futures": "Futures Mode",
|
|
85
|
+
"okx:multiCurrencyMargin": "Multi-Currency Margin",
|
|
86
|
+
"okx:portfolioMargin": "Portfolio Margin"
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { ACCOUNT_MODE_LABELS, AccountBalanceCore, AccountMode, CollateralBalance, DecimalString, MarginMode, Side, SignedDecimalString };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BinanceSingleAssetBalance, BinanceMultiAssetBalance } from './binance/balance';
|
|
3
|
+
import { BybitIsolatedMarginBalance, BybitRegularMarginBalance, BybitPortfolioMarginBalance } from './bybit/balance';
|
|
4
|
+
import { HyperliquidStandardBalance, HyperliquidUnifiedBalance, HyperliquidPortfolioMarginBalance } from './hyperliquid/balance';
|
|
5
|
+
import { LighterClassicBalance, LighterUnifiedBalance } from './lighter/balance';
|
|
6
|
+
import { OkxSpotBalance, OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance } from './okx/balance';
|
|
7
|
+
|
|
8
|
+
const AccountBalance = z.discriminatedUnion("accountMode", [
|
|
9
|
+
HyperliquidStandardBalance,
|
|
10
|
+
HyperliquidUnifiedBalance,
|
|
11
|
+
HyperliquidPortfolioMarginBalance,
|
|
12
|
+
LighterClassicBalance,
|
|
13
|
+
LighterUnifiedBalance,
|
|
14
|
+
BinanceSingleAssetBalance,
|
|
15
|
+
BinanceMultiAssetBalance,
|
|
16
|
+
BybitIsolatedMarginBalance,
|
|
17
|
+
BybitRegularMarginBalance,
|
|
18
|
+
BybitPortfolioMarginBalance,
|
|
19
|
+
OkxSpotBalance,
|
|
20
|
+
OkxFuturesBalance,
|
|
21
|
+
OkxMultiCurrencyMarginBalance,
|
|
22
|
+
OkxPortfolioMarginBalance
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export { AccountBalance };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Side, DecimalString, MarginMode } from './balance-core';
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
const BasketLeg = z.object({
|
|
8
|
+
asset: z.string(),
|
|
9
|
+
marginMode: MarginMode,
|
|
10
|
+
leverage: DecimalString,
|
|
11
|
+
side: Side,
|
|
12
|
+
/** Unsigned percentage. The set sums to 100. */
|
|
13
|
+
weight: z.number().positive()
|
|
14
|
+
});
|
|
15
|
+
const MaxBasketSizeInput = z.object({
|
|
16
|
+
legs: z.array(BasketLeg).min(1)
|
|
17
|
+
});
|
|
18
|
+
const MaxBasketSizeResult = z.object({
|
|
19
|
+
/**
|
|
20
|
+
* The whole basket's max GROSS notional in USD — Σ leg notional, not per-side and not per-asset.
|
|
21
|
+
* The basket is atomic, so this is one ceiling for the entire structure.
|
|
22
|
+
* Null when the venue cannot supply a per-asset max leverage (all three CEXs in v1), and null
|
|
23
|
+
* when any leg draws on a pool the caller could not value.
|
|
24
|
+
*/
|
|
25
|
+
maxNotional: DecimalString.nullable(),
|
|
26
|
+
/** Derived from maxNotional — each leg's notional at that basket size. NOT a per-leg maximum. */
|
|
27
|
+
legs: z.array(z.object({
|
|
28
|
+
asset: z.string(),
|
|
29
|
+
notional: DecimalString,
|
|
30
|
+
marginRequired: DecimalString
|
|
31
|
+
})),
|
|
32
|
+
/** Which pool or leg ran out first. */
|
|
33
|
+
bindingConstraint: z.discriminatedUnion("kind", [
|
|
34
|
+
z.object({
|
|
35
|
+
kind: z.literal("cross-pool"),
|
|
36
|
+
pool: z.string()
|
|
37
|
+
}),
|
|
38
|
+
z.object({
|
|
39
|
+
kind: z.literal("isolated-leg"),
|
|
40
|
+
asset: z.string()
|
|
41
|
+
})
|
|
42
|
+
]).nullable()
|
|
43
|
+
});
|
|
44
|
+
function computeMaxBasketSize(input) {
|
|
45
|
+
if (!isSizeableInput(input)) {
|
|
46
|
+
return unsizedResult();
|
|
47
|
+
}
|
|
48
|
+
const budgets = new Map(Object.entries(input.budgets));
|
|
49
|
+
const weightSum = sumWeights(input.legs);
|
|
50
|
+
if (weightSum.isZero()) {
|
|
51
|
+
return unsizedResult();
|
|
52
|
+
}
|
|
53
|
+
const shares = input.legs.map((leg) => toLegShare(leg, weightSum));
|
|
54
|
+
const demandByPool = marginPerUnitNotionalByPool(shares);
|
|
55
|
+
const tightestPool = tightestPoolCeiling(demandByPool, budgets);
|
|
56
|
+
if (tightestPool === null) {
|
|
57
|
+
return unsizedResult();
|
|
58
|
+
}
|
|
59
|
+
const tightestCap = tightestLegCap(shares);
|
|
60
|
+
if (tightestCap === null || tightestPool.size.isLessThanOrEqualTo(tightestCap.size)) {
|
|
61
|
+
return buildResult(tightestPool.size, shares, {
|
|
62
|
+
kind: "cross-pool",
|
|
63
|
+
pool: tightestPool.pool
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return buildResult(tightestCap.size, shares, {
|
|
67
|
+
kind: "isolated-leg",
|
|
68
|
+
asset: tightestCap.asset
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
__name(computeMaxBasketSize, "computeMaxBasketSize");
|
|
72
|
+
function isSizeableInput(input) {
|
|
73
|
+
for (const budget of Object.values(input.budgets)) {
|
|
74
|
+
if (!isUnsignedDecimal(budget)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const leg of input.legs) {
|
|
79
|
+
if (!isPositiveFinite(leg.weight)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!isUsableLeverage(leg.effectiveLeverage)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (leg.maxNotional !== null && !isUnsignedDecimal(leg.maxNotional)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
__name(isSizeableInput, "isSizeableInput");
|
|
92
|
+
function isUnsignedDecimal(value) {
|
|
93
|
+
return DecimalString.safeParse(value).success;
|
|
94
|
+
}
|
|
95
|
+
__name(isUnsignedDecimal, "isUnsignedDecimal");
|
|
96
|
+
function isPositiveFinite(weight) {
|
|
97
|
+
return Number.isFinite(weight) && weight > 0;
|
|
98
|
+
}
|
|
99
|
+
__name(isPositiveFinite, "isPositiveFinite");
|
|
100
|
+
function isUsableLeverage(effectiveLeverage) {
|
|
101
|
+
if (!isUnsignedDecimal(effectiveLeverage)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return !new BigNumber(effectiveLeverage).isZero();
|
|
105
|
+
}
|
|
106
|
+
__name(isUsableLeverage, "isUsableLeverage");
|
|
107
|
+
function sumWeights(legs) {
|
|
108
|
+
let total = new BigNumber(0);
|
|
109
|
+
for (const leg of legs) {
|
|
110
|
+
total = total.plus(String(leg.weight));
|
|
111
|
+
}
|
|
112
|
+
return total;
|
|
113
|
+
}
|
|
114
|
+
__name(sumWeights, "sumWeights");
|
|
115
|
+
function unsizedResult() {
|
|
116
|
+
return {
|
|
117
|
+
maxNotional: null,
|
|
118
|
+
legs: [],
|
|
119
|
+
bindingConstraint: null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
__name(unsizedResult, "unsizedResult");
|
|
123
|
+
function toLegShare(leg, weightSum) {
|
|
124
|
+
return {
|
|
125
|
+
asset: leg.asset,
|
|
126
|
+
pool: leg.pool,
|
|
127
|
+
share: new BigNumber(String(leg.weight)).div(weightSum),
|
|
128
|
+
imr: new BigNumber(1).div(leg.effectiveLeverage),
|
|
129
|
+
maxNotional: leg.maxNotional === null ? null : new BigNumber(leg.maxNotional)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
__name(toLegShare, "toLegShare");
|
|
133
|
+
function marginPerUnitNotionalByPool(shares) {
|
|
134
|
+
const demandByPool = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const leg of shares) {
|
|
136
|
+
const running = demandByPool.get(leg.pool) ?? new BigNumber(0);
|
|
137
|
+
demandByPool.set(leg.pool, running.plus(leg.share.times(leg.imr)));
|
|
138
|
+
}
|
|
139
|
+
return demandByPool;
|
|
140
|
+
}
|
|
141
|
+
__name(marginPerUnitNotionalByPool, "marginPerUnitNotionalByPool");
|
|
142
|
+
function tightestPoolCeiling(demandByPool, budgets) {
|
|
143
|
+
let tightest = null;
|
|
144
|
+
for (const [pool, marginPerUnitNotional] of demandByPool) {
|
|
145
|
+
const budget = budgets.get(pool);
|
|
146
|
+
if (budget === void 0) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const size = new BigNumber(budget).div(marginPerUnitNotional);
|
|
150
|
+
if (tightest === null || size.isLessThan(tightest.size)) {
|
|
151
|
+
tightest = {
|
|
152
|
+
pool,
|
|
153
|
+
size
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return tightest;
|
|
158
|
+
}
|
|
159
|
+
__name(tightestPoolCeiling, "tightestPoolCeiling");
|
|
160
|
+
function tightestLegCap(shares) {
|
|
161
|
+
let tightest = null;
|
|
162
|
+
for (const leg of shares) {
|
|
163
|
+
if (leg.maxNotional === null) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const size = leg.maxNotional.div(leg.share);
|
|
167
|
+
if (tightest === null || size.isLessThan(tightest.size)) {
|
|
168
|
+
tightest = {
|
|
169
|
+
asset: leg.asset,
|
|
170
|
+
size
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return tightest;
|
|
175
|
+
}
|
|
176
|
+
__name(tightestLegCap, "tightestLegCap");
|
|
177
|
+
function buildResult(size, shares, bindingConstraint) {
|
|
178
|
+
const legs = shares.map((leg) => {
|
|
179
|
+
const notional = size.times(leg.share);
|
|
180
|
+
return {
|
|
181
|
+
asset: leg.asset,
|
|
182
|
+
notional: notional.toFixed(),
|
|
183
|
+
marginRequired: notional.times(leg.imr).toFixed()
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
maxNotional: size.toFixed(),
|
|
188
|
+
legs,
|
|
189
|
+
bindingConstraint
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
__name(buildResult, "buildResult");
|
|
193
|
+
|
|
194
|
+
export { BasketLeg, MaxBasketSizeInput, MaxBasketSizeResult, computeMaxBasketSize };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AccountBalanceCore } from '../balance-core';
|
|
3
|
+
|
|
4
|
+
const BinanceSingleAssetBalance = AccountBalanceCore.extend({
|
|
5
|
+
accountMode: z.literal("binance:singleAsset")
|
|
6
|
+
});
|
|
7
|
+
const BinanceMultiAssetBalance = AccountBalanceCore.extend({
|
|
8
|
+
accountMode: z.literal("binance:multiAsset")
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export { BinanceMultiAssetBalance, BinanceSingleAssetBalance };
|
|
@@ -3,48 +3,42 @@ import { positionKey } from '../utils';
|
|
|
3
3
|
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
6
|
+
const DECIMAL_PATTERN = /^\d+(\.\d+)?$/;
|
|
7
|
+
const ACCOUNT_LEVEL_ASSET = "USDT";
|
|
8
|
+
const SIGNED_DECIMAL_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
9
|
+
function toBinanceAccountMode(multiAssetsMargin) {
|
|
10
|
+
return multiAssetsMargin ? "binance:multiAsset" : "binance:singleAsset";
|
|
11
|
+
}
|
|
12
|
+
__name(toBinanceAccountMode, "toBinanceAccountMode");
|
|
13
|
+
function mapBalance(raw, accountMode, timestamp) {
|
|
14
|
+
const assets = raw.assets ?? [];
|
|
15
|
+
const perCollateral = toCollateralRows(assets);
|
|
16
|
+
switch (accountMode) {
|
|
17
|
+
// Multi-asset mode values the whole wallet in USD, so the account-level figures are complete.
|
|
18
|
+
case "binance:multiAsset":
|
|
19
|
+
return {
|
|
20
|
+
accountMode,
|
|
21
|
+
totalEquity: decimalOrNull(raw.totalMarginBalance),
|
|
22
|
+
withdrawableBalance: decimalOrNull(raw.maxWithdrawAmount),
|
|
23
|
+
availableMargin: decimalOrNull(raw.availableBalance),
|
|
24
|
+
poolsShared: true,
|
|
25
|
+
perCollateral,
|
|
26
|
+
timestamp
|
|
27
|
+
};
|
|
28
|
+
// Single-asset mode runs a pool per settlement currency, so its scalars need the pool test.
|
|
29
|
+
case "binance:singleAsset": {
|
|
30
|
+
const scalarsDescribeTheAccount = nothingFundedOutsideUsdt(assets);
|
|
31
|
+
return {
|
|
32
|
+
accountMode,
|
|
33
|
+
totalEquity: scalarsDescribeTheAccount ? decimalOrNull(raw.totalMarginBalance) : null,
|
|
34
|
+
withdrawableBalance: scalarsDescribeTheAccount ? decimalOrNull(raw.maxWithdrawAmount) : null,
|
|
35
|
+
availableMargin: scalarsDescribeTheAccount ? decimalOrNull(raw.availableBalance) : null,
|
|
36
|
+
poolsShared: false,
|
|
37
|
+
perCollateral,
|
|
38
|
+
timestamp
|
|
39
|
+
};
|
|
40
|
+
}
|
|
28
41
|
}
|
|
29
|
-
const totalMarginBalance = raw.totalMarginBalance ?? "0";
|
|
30
|
-
const totalMaintMargin = raw.totalMaintMargin ?? "0";
|
|
31
|
-
const marginBalanceBN = new BigNumber(totalMarginBalance);
|
|
32
|
-
const maintMarginBN = new BigNumber(totalMaintMargin);
|
|
33
|
-
const marginRatio = marginBalanceBN.gt(0) ? maintMarginBN.div(marginBalanceBN).toFixed(4) : "0.0000";
|
|
34
|
-
return {
|
|
35
|
-
free,
|
|
36
|
-
used,
|
|
37
|
-
total,
|
|
38
|
-
availableToTrade: raw.availableBalance ?? "0",
|
|
39
|
-
totalEquity: totalMarginBalance,
|
|
40
|
-
walletBalance: raw.totalWalletBalance ?? "0",
|
|
41
|
-
unrealizedPnl: raw.totalUnrealizedProfit ?? "0",
|
|
42
|
-
initialMarginUsed: raw.totalInitialMargin ?? "0",
|
|
43
|
-
maintenanceMargin: totalMaintMargin,
|
|
44
|
-
marginRatio,
|
|
45
|
-
assets,
|
|
46
|
-
timestamp: Date.now()
|
|
47
|
-
};
|
|
48
42
|
}
|
|
49
43
|
__name(mapBalance, "mapBalance");
|
|
50
44
|
function mapPositions(raw) {
|
|
@@ -86,5 +80,56 @@ function mergePositions(incoming, existing) {
|
|
|
86
80
|
return merged;
|
|
87
81
|
}
|
|
88
82
|
__name(mergePositions, "mergePositions");
|
|
83
|
+
function toCollateralRows(assets) {
|
|
84
|
+
const rows = [];
|
|
85
|
+
for (const asset of assets) {
|
|
86
|
+
if (!isFunded(asset.marginBalance)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
rows.push({
|
|
90
|
+
asset: asset.asset,
|
|
91
|
+
equity: asset.marginBalance,
|
|
92
|
+
availableMargin: decimalOrNull(asset.availableBalance),
|
|
93
|
+
withdrawable: decimalOrNull(asset.maxWithdrawAmount)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return rows.sort(byAssetName);
|
|
97
|
+
}
|
|
98
|
+
__name(toCollateralRows, "toCollateralRows");
|
|
99
|
+
function nothingFundedOutsideUsdt(assets) {
|
|
100
|
+
for (const asset of assets) {
|
|
101
|
+
if (asset.asset !== ACCOUNT_LEVEL_ASSET && holdsAnyBalance(asset.marginBalance)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
__name(nothingFundedOutsideUsdt, "nothingFundedOutsideUsdt");
|
|
108
|
+
function holdsAnyBalance(value) {
|
|
109
|
+
if (value === void 0 || !SIGNED_DECIMAL_PATTERN.test(value)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return !new BigNumber(value).isZero();
|
|
113
|
+
}
|
|
114
|
+
__name(holdsAnyBalance, "holdsAnyBalance");
|
|
115
|
+
function isFunded(value) {
|
|
116
|
+
return isDecimal(value) && !new BigNumber(value).isZero();
|
|
117
|
+
}
|
|
118
|
+
__name(isFunded, "isFunded");
|
|
119
|
+
function isDecimal(value) {
|
|
120
|
+
return value !== void 0 && DECIMAL_PATTERN.test(value);
|
|
121
|
+
}
|
|
122
|
+
__name(isDecimal, "isDecimal");
|
|
123
|
+
function byAssetName(a, b) {
|
|
124
|
+
if (a.asset === b.asset) {
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
return a.asset < b.asset ? -1 : 1;
|
|
128
|
+
}
|
|
129
|
+
__name(byAssetName, "byAssetName");
|
|
130
|
+
function decimalOrNull(value) {
|
|
131
|
+
return isDecimal(value) ? value : null;
|
|
132
|
+
}
|
|
133
|
+
__name(decimalOrNull, "decimalOrNull");
|
|
89
134
|
|
|
90
|
-
export { mapBalance, mapPositions, mergePositions };
|
|
135
|
+
export { mapBalance, mapPositions, mergePositions, toBinanceAccountMode };
|
|
@@ -3,8 +3,7 @@ import { fetchAllPositionRisk, keepAliveListenKey, fetchAccountStatus, fetchMult
|
|
|
3
3
|
import { BaseAccountTracker } from '../base';
|
|
4
4
|
import { positionKey } from '../utils';
|
|
5
5
|
import { DEFAULT_LISTEN_KEY_REFRESH_MS, DEFAULT_ACCOUNT_POLL_MS } from './const';
|
|
6
|
-
import { mapBalance, mapPositions, mergePositions } from './mapper';
|
|
7
|
-
import { BinanceAccountType } from './types';
|
|
6
|
+
import { mapBalance, toBinanceAccountMode, mapPositions, mergePositions } from './mapper';
|
|
8
7
|
|
|
9
8
|
var __defProp = Object.defineProperty;
|
|
10
9
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -22,7 +21,7 @@ class BinanceAccountTracker extends BaseAccountTracker {
|
|
|
22
21
|
listenKeyTimer = null;
|
|
23
22
|
pollTimer = null;
|
|
24
23
|
currentPositions = /* @__PURE__ */ new Map();
|
|
25
|
-
|
|
24
|
+
accountMode = null;
|
|
26
25
|
trackedAssetInfos = /* @__PURE__ */ new Map();
|
|
27
26
|
constructor(config) {
|
|
28
27
|
super(config);
|
|
@@ -32,17 +31,26 @@ class BinanceAccountTracker extends BaseAccountTracker {
|
|
|
32
31
|
}
|
|
33
32
|
async snapshot() {
|
|
34
33
|
const { snapshot, positionRisk, multiAssetsMargin } = await this.fetchSnapshotData();
|
|
35
|
-
const
|
|
36
|
-
const balance = {
|
|
37
|
-
...mapBalance(snapshot),
|
|
38
|
-
accountType
|
|
39
|
-
};
|
|
34
|
+
const balance = mapBalance(snapshot, toBinanceAccountMode(multiAssetsMargin), Date.now());
|
|
40
35
|
return {
|
|
41
36
|
balance,
|
|
42
37
|
positions: Array.from(mapPositions(positionRisk).values())
|
|
43
38
|
};
|
|
44
39
|
}
|
|
45
40
|
/**
|
|
41
|
+
* Binance sizes no basket in v1: the SDK reads no per-asset max leverage from it, and sizing off
|
|
42
|
+
* `availableMargin` alone would ship a number no venue field backs. The missing input is
|
|
43
|
+
* `/fapi/v2/positionRisk` → `maxNotionalValue` (the account's own per-symbol ceiling), with
|
|
44
|
+
* `/fapi/v1/leverageBracket` behind it for the tier a larger size would fall into.
|
|
45
|
+
*/
|
|
46
|
+
async maxBasketSize(_input) {
|
|
47
|
+
return unsizedBasket();
|
|
48
|
+
}
|
|
49
|
+
/** The REST path answers the same way — there is no size either way, not a slower one. */
|
|
50
|
+
async snapshotMaxBasketSize(_input) {
|
|
51
|
+
return unsizedBasket();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
46
54
|
* Stateless one-shot per-asset settings read: `positionRisk` with no symbol returns leverage +
|
|
47
55
|
* margin type for every symbol (positioned or not), so any number of assets costs one call.
|
|
48
56
|
*/
|
|
@@ -61,7 +69,7 @@ class BinanceAccountTracker extends BaseAccountTracker {
|
|
|
61
69
|
}
|
|
62
70
|
async start() {
|
|
63
71
|
const { snapshot, positionRisk, multiAssetsMargin } = await this.fetchSnapshotData();
|
|
64
|
-
this.
|
|
72
|
+
this.accountMode = toBinanceAccountMode(multiAssetsMargin);
|
|
65
73
|
this.handleAccountStatus(snapshot);
|
|
66
74
|
this.handlePositionRisk(positionRisk, false);
|
|
67
75
|
const ws = await createExchangeWs("binance", {
|
|
@@ -157,12 +165,11 @@ class BinanceAccountTracker extends BaseAccountTracker {
|
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
handleAccountStatus(result) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
168
|
+
if (this.accountMode === null) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
164
171
|
this.onStateUpdate({
|
|
165
|
-
balance
|
|
172
|
+
balance: mapBalance(result, this.accountMode, Date.now())
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
175
|
handlePositionRisk(risk, merge) {
|
|
@@ -247,5 +254,13 @@ function toTrackedAssetInfo(coin, risk) {
|
|
|
247
254
|
};
|
|
248
255
|
}
|
|
249
256
|
__name(toTrackedAssetInfo, "toTrackedAssetInfo");
|
|
257
|
+
function unsizedBasket() {
|
|
258
|
+
return {
|
|
259
|
+
maxNotional: null,
|
|
260
|
+
legs: [],
|
|
261
|
+
bindingConstraint: null
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
__name(unsizedBasket, "unsizedBasket");
|
|
250
265
|
|
|
251
266
|
export { BinanceAccountTracker };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AccountBalanceCore } from '../balance-core';
|
|
3
|
+
|
|
4
|
+
const BybitIsolatedMarginBalance = AccountBalanceCore.extend({
|
|
5
|
+
accountMode: z.literal("bybit:isolatedMargin")
|
|
6
|
+
});
|
|
7
|
+
const BybitRegularMarginBalance = AccountBalanceCore.extend({
|
|
8
|
+
accountMode: z.literal("bybit:regularMargin")
|
|
9
|
+
});
|
|
10
|
+
const BybitPortfolioMarginBalance = AccountBalanceCore.extend({
|
|
11
|
+
accountMode: z.literal("bybit:portfolioMargin")
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export { BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance };
|