@pear-protocol/exchanges-sdk 0.6.7 → 0.8.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/index.d.ts +1131 -78
- package/dist/sdk.js +9 -2
- package/dist/syncer/base.js +36 -14
- 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) {
|
package/dist/syncer/base.js
CHANGED
|
@@ -99,10 +99,24 @@ class BaseAccountSyncer {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
if (payload.reconcilables) {
|
|
102
|
-
|
|
102
|
+
const reconcileBytes = new Blob([
|
|
103
|
+
JSON.stringify(payload.reconcilables)
|
|
104
|
+
]).size;
|
|
105
|
+
if (reconcileBytes > 50 * 1024) {
|
|
106
|
+
await this.sdk.core.sync.fills.reconcileBulk(payload.reconcilables);
|
|
107
|
+
} else {
|
|
108
|
+
await this.sdk.core.sync.fills.reconcile(payload.reconcilables);
|
|
109
|
+
}
|
|
103
110
|
}
|
|
104
111
|
if (payload.confirmables) {
|
|
105
|
-
|
|
112
|
+
const confirmBytes = new Blob([
|
|
113
|
+
JSON.stringify(payload.confirmables)
|
|
114
|
+
]).size;
|
|
115
|
+
if (confirmBytes > 50 * 1024) {
|
|
116
|
+
await this.sdk.core.sync.fills.confirmBulk(payload.confirmables);
|
|
117
|
+
} else {
|
|
118
|
+
await this.sdk.core.sync.fills.confirm(payload.confirmables);
|
|
119
|
+
}
|
|
106
120
|
}
|
|
107
121
|
const { firstSyncTrim } = payload;
|
|
108
122
|
let meta = {
|
|
@@ -128,14 +142,14 @@ class BaseAccountSyncer {
|
|
|
128
142
|
exchangePositions: payload.exchangePositions,
|
|
129
143
|
meta
|
|
130
144
|
};
|
|
131
|
-
const
|
|
145
|
+
const createBytes = new Blob([
|
|
132
146
|
JSON.stringify(createBody)
|
|
133
147
|
]).size;
|
|
134
|
-
if (
|
|
148
|
+
if (createBytes > 50 * 1024) {
|
|
135
149
|
await this.sdk.core.sync.fills.createBulk(createBody);
|
|
136
|
-
|
|
150
|
+
} else {
|
|
151
|
+
await this.sdk.core.sync.fills.create(createBody);
|
|
137
152
|
}
|
|
138
|
-
await this.sdk.core.sync.fills.create(createBody);
|
|
139
153
|
}
|
|
140
154
|
/**
|
|
141
155
|
* Pull historical funding payments since the funding watermark and post them.
|
|
@@ -151,12 +165,18 @@ class BaseAccountSyncer {
|
|
|
151
165
|
overlapMs: 6e4
|
|
152
166
|
})
|
|
153
167
|
]);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
let payments;
|
|
169
|
+
try {
|
|
170
|
+
payments = await this.listExternalFunding({
|
|
171
|
+
since: new Date(watermark.sinceMs),
|
|
172
|
+
instruments
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`${this.connector}: funding fetch failed \u2014 funding sync aborted, retry next tick`, error);
|
|
176
|
+
return "aborted";
|
|
177
|
+
}
|
|
158
178
|
if (payments.length === 0) {
|
|
159
|
-
return;
|
|
179
|
+
return "synced";
|
|
160
180
|
}
|
|
161
181
|
const externalMaxTs = Math.max(...payments.map((payment) => payment.timestamp));
|
|
162
182
|
const createBody = {
|
|
@@ -170,9 +190,10 @@ class BaseAccountSyncer {
|
|
|
170
190
|
]).size;
|
|
171
191
|
if (bytes > 50 * 1024) {
|
|
172
192
|
await this.sdk.core.sync.funding.createBulk(createBody);
|
|
173
|
-
return;
|
|
193
|
+
return "synced";
|
|
174
194
|
}
|
|
175
195
|
await this.sdk.core.sync.funding.create(createBody);
|
|
196
|
+
return "synced";
|
|
176
197
|
}
|
|
177
198
|
async synchronizeFills() {
|
|
178
199
|
const state = await this.loadPearState();
|
|
@@ -181,17 +202,18 @@ class BaseAccountSyncer {
|
|
|
181
202
|
snapshot = await this.snapshotExchange(state);
|
|
182
203
|
} catch (error) {
|
|
183
204
|
console.error(`${this.connector}: position/fill fetch failed \u2014 sync cycle aborted, retry next tick`, error);
|
|
184
|
-
return;
|
|
205
|
+
return "aborted";
|
|
185
206
|
}
|
|
186
207
|
try {
|
|
187
208
|
await this.computeAndPostSync(state, snapshot);
|
|
188
209
|
} catch (error) {
|
|
189
210
|
if (isSyncCreateDeferral(error)) {
|
|
190
211
|
console.log(`${this.connector}: sync cycle deferred by the server (${error.errorCode}) \u2014 retry next tick`);
|
|
191
|
-
return;
|
|
212
|
+
return "deferred";
|
|
192
213
|
}
|
|
193
214
|
throw error;
|
|
194
215
|
}
|
|
216
|
+
return "synced";
|
|
195
217
|
}
|
|
196
218
|
}
|
|
197
219
|
function isSyncCreateDeferral(error) {
|
|
@@ -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 };
|