@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.
Files changed (41) hide show
  1. package/README.md +105 -25
  2. package/dist/client/bybit/rest.js +8 -1
  3. package/dist/client/hyperliquid/rest.js +21 -1
  4. package/dist/client/lighter/rest.js +6 -12
  5. package/dist/client/okx/rest.js +5 -1
  6. package/dist/index.d.ts +1131 -78
  7. package/dist/sdk.js +9 -2
  8. package/dist/syncer/base.js +36 -14
  9. package/dist/tracker/balance-core.js +89 -0
  10. package/dist/tracker/balance.js +25 -0
  11. package/dist/tracker/basket-size.js +194 -0
  12. package/dist/tracker/binance/balance.js +11 -0
  13. package/dist/tracker/binance/mapper.js +87 -42
  14. package/dist/tracker/binance/tracker.js +29 -14
  15. package/dist/tracker/binance/types.js +0 -6
  16. package/dist/tracker/bybit/balance.js +14 -0
  17. package/dist/tracker/bybit/mapper.js +109 -52
  18. package/dist/tracker/bybit/tracker.js +242 -33
  19. package/dist/tracker/bybit/types.js +0 -7
  20. package/dist/tracker/hyperliquid/account-mode.js +14 -0
  21. package/dist/tracker/hyperliquid/balance.js +184 -0
  22. package/dist/tracker/hyperliquid/basket-size.js +93 -0
  23. package/dist/tracker/hyperliquid/coin-name.js +18 -0
  24. package/dist/tracker/hyperliquid/collateral.js +48 -0
  25. package/dist/tracker/hyperliquid/decimal.js +28 -0
  26. package/dist/tracker/hyperliquid/mapper.js +399 -176
  27. package/dist/tracker/hyperliquid/spot-pricing.js +106 -0
  28. package/dist/tracker/hyperliquid/tracker.js +237 -78
  29. package/dist/tracker/hyperliquid/types.js +1 -7
  30. package/dist/tracker/index.js +9 -7
  31. package/dist/tracker/lighter/balance.js +87 -0
  32. package/dist/tracker/lighter/basket-size.js +74 -0
  33. package/dist/tracker/lighter/mapper.js +324 -145
  34. package/dist/tracker/lighter/tracker.js +228 -117
  35. package/dist/tracker/lighter/types.js +1 -7
  36. package/dist/tracker/okx/balance.js +17 -0
  37. package/dist/tracker/okx/mapper.js +156 -58
  38. package/dist/tracker/okx/tracker.js +228 -29
  39. package/dist/tracker/okx/types.js +0 -8
  40. package/dist/tracker/types.js +0 -24
  41. 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) {
@@ -99,10 +99,24 @@ class BaseAccountSyncer {
99
99
  return;
100
100
  }
101
101
  if (payload.reconcilables) {
102
- await this.sdk.core.sync.fills.reconcile(payload.reconcilables);
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
- await this.sdk.core.sync.fills.confirm(payload.confirmables);
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 bytes = new Blob([
145
+ const createBytes = new Blob([
132
146
  JSON.stringify(createBody)
133
147
  ]).size;
134
- if (bytes > 50 * 1024) {
148
+ if (createBytes > 50 * 1024) {
135
149
  await this.sdk.core.sync.fills.createBulk(createBody);
136
- return;
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
- const payments = await this.listExternalFunding({
155
- since: new Date(watermark.sinceMs),
156
- instruments
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
- function mapBalance(raw) {
7
- const free = {};
8
- const used = {};
9
- const total = {};
10
- const assets = [];
11
- for (const a of raw.assets ?? []) {
12
- const wb = a.walletBalance ?? "0";
13
- const ab = a.availableBalance ?? "0";
14
- const mb = a.marginBalance ?? "0";
15
- const im = a.initialMargin ?? "0";
16
- if (new BigNumber(wb).isZero() && new BigNumber(ab).isZero() && new BigNumber(mb).isZero()) continue;
17
- free[a.asset] = ab;
18
- used[a.asset] = im;
19
- total[a.asset] = mb;
20
- assets.push({
21
- asset: a.asset,
22
- free: ab,
23
- used: im,
24
- total: mb,
25
- walletBalance: wb,
26
- usdValue: mb
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
- accountType = BinanceAccountType.SINGLE_ASSET;
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 accountType = multiAssetsMargin ? BinanceAccountType.MULTI_ASSET : BinanceAccountType.SINGLE_ASSET;
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.accountType = multiAssetsMargin ? BinanceAccountType.MULTI_ASSET : BinanceAccountType.SINGLE_ASSET;
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
- const balance = {
161
- ...mapBalance(result),
162
- accountType: this.accountType
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 };
@@ -1,7 +1 @@
1
- var BinanceAccountType = /* @__PURE__ */ (function(BinanceAccountType2) {
2
- BinanceAccountType2["MULTI_ASSET"] = "MULTI_ASSET";
3
- BinanceAccountType2["SINGLE_ASSET"] = "SINGLE_ASSET";
4
- return BinanceAccountType2;
5
- })({});
6
1
 
7
- export { BinanceAccountType };