@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
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import { createExchangeWs } from '../../client';
|
|
3
|
-
import { fetchOrderBookDetails, fetchAccount
|
|
4
|
-
import { retrieveLighterAuthToken } from '../../helper/lighter/auth-token.helper';
|
|
3
|
+
import { fetchOrderBookDetails, fetchAccount } from '../../client/lighter/rest';
|
|
5
4
|
import { BaseAccountTracker } from '../base';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { computeMaxBasketSize } from '../basket-size';
|
|
6
|
+
import { toLighterBasketCalculation } from './basket-size';
|
|
7
|
+
import { buildSpotPriceIndex, mapPositions, mapRestBalance, mapMarketDefaultAssetInfo, toLighterAccountType, mapLiveBalance } from './mapper';
|
|
8
8
|
|
|
9
9
|
var __defProp = Object.defineProperty;
|
|
10
10
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
11
|
-
const
|
|
11
|
+
const POSITION_POLL_INTERVAL_MS = 2e3;
|
|
12
|
+
const MARKET_REFRESH_INTERVAL_MS = 6e4;
|
|
13
|
+
const DECIMAL_PATTERN = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
14
|
+
function isZeroSize(size) {
|
|
15
|
+
if (!DECIMAL_PATTERN.test(size)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return new BigNumber(size).isZero();
|
|
19
|
+
}
|
|
20
|
+
__name(isZeroSize, "isZeroSize");
|
|
12
21
|
function toPosRecord(positions) {
|
|
13
22
|
const posRecord = {};
|
|
14
23
|
for (const p of positions) {
|
|
@@ -22,45 +31,44 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
22
31
|
__name(this, "LighterAccountTracker");
|
|
23
32
|
}
|
|
24
33
|
accountIdentifier;
|
|
25
|
-
sdk;
|
|
26
34
|
ws = null;
|
|
27
35
|
messageListenerId = null;
|
|
28
36
|
openListenerId = null;
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
positionPollTimer = null;
|
|
38
|
+
positionPollInFlight = null;
|
|
39
|
+
marketRefreshTimer = null;
|
|
40
|
+
marketRefreshInFlight = null;
|
|
31
41
|
startInFlight = null;
|
|
32
42
|
lifecycleVersion = 0;
|
|
33
43
|
stateVersion = 0;
|
|
34
44
|
assetsByAssetId = /* @__PURE__ */ new Map();
|
|
35
|
-
|
|
45
|
+
// Written only by `account_all`. The live balance reads its unrealized PnL and maintenance
|
|
46
|
+
// margin from here, so keeping REST out of it is what makes that balance wholly websocket-derived.
|
|
47
|
+
wsPositionsByMarketId = /* @__PURE__ */ new Map();
|
|
36
48
|
marketsByMarketId = /* @__PURE__ */ new Map();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
marketsBySymbol = /* @__PURE__ */ new Map();
|
|
50
|
+
// Seeded empty, which still prices the quote asset — spot dollars are dollars before any
|
|
51
|
+
// market data arrives, and a balance the index cannot price would otherwise read as unknown.
|
|
52
|
+
spotPriceBySymbol = buildSpotPriceIndex([]);
|
|
53
|
+
stats = null;
|
|
40
54
|
hasAssets = false;
|
|
41
55
|
constructor(config) {
|
|
42
56
|
super(config);
|
|
43
57
|
this.accountIdentifier = config.account.exchangeIdentifier;
|
|
44
|
-
this.sdk = config.sdk;
|
|
45
58
|
}
|
|
46
|
-
/** Stateless one-shot account read
|
|
59
|
+
/** Stateless one-shot account read — public endpoints only, and no websocket. */
|
|
47
60
|
async snapshot() {
|
|
48
|
-
const [account, markets
|
|
61
|
+
const [account, markets] = await Promise.all([
|
|
49
62
|
this.fetchPrimaryAccount(),
|
|
50
|
-
fetchOrderBookDetails(this.demo)
|
|
51
|
-
this.fetchAccountTier()
|
|
63
|
+
fetchOrderBookDetails(this.demo)
|
|
52
64
|
]);
|
|
53
65
|
if (!account) return {
|
|
54
66
|
balance: null,
|
|
55
67
|
positions: []
|
|
56
68
|
};
|
|
57
69
|
const { positions } = mapPositions(toPosRecord(account.positions ?? []));
|
|
58
|
-
const marketsByMarketId = new Map(markets.map((market) => [
|
|
59
|
-
market.market_id,
|
|
60
|
-
market
|
|
61
|
-
]));
|
|
62
70
|
return {
|
|
63
|
-
balance:
|
|
71
|
+
balance: mapRestBalance(account, toMarketContext(markets.perp, markets.spot), Date.now()),
|
|
64
72
|
positions: Array.from(positions.values())
|
|
65
73
|
};
|
|
66
74
|
}
|
|
@@ -75,22 +83,48 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
75
83
|
this.fetchPrimaryAccount(),
|
|
76
84
|
fetchOrderBookDetails(this.demo)
|
|
77
85
|
]);
|
|
78
|
-
const
|
|
79
|
-
const defaults = /* @__PURE__ */ new Map();
|
|
80
|
-
for (const m of markets) {
|
|
81
|
-
defaults.set(String(m.market_id), mapMarketDefaultAssetInfo(m));
|
|
82
|
-
}
|
|
86
|
+
const settings = toAccountSettings(markets.perp, account?.positions ?? []);
|
|
83
87
|
const result = {};
|
|
84
88
|
for (const marketId of marketIds) {
|
|
85
|
-
result[marketId] =
|
|
89
|
+
result[marketId] = settings.get(marketId) ?? null;
|
|
86
90
|
}
|
|
87
91
|
return result;
|
|
88
92
|
}
|
|
93
|
+
/** Sizes a basket against the live account state, falling back to REST before the socket is up. */
|
|
94
|
+
async maxBasketSize(input) {
|
|
95
|
+
if (!this.stats || this.marketsBySymbol.size === 0) {
|
|
96
|
+
return this.snapshotMaxBasketSize(input);
|
|
97
|
+
}
|
|
98
|
+
return computeMaxBasketSize(toLighterBasketCalculation(input.legs, {
|
|
99
|
+
availableBalance: this.stats.total.available_balance,
|
|
100
|
+
perpBySymbol: this.marketsBySymbol,
|
|
101
|
+
settingsByMarketId: this.liveAccountSettings()
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/** Sizes a basket over REST alone, so the answer needs no running socket. */
|
|
105
|
+
async snapshotMaxBasketSize(input) {
|
|
106
|
+
const [account, markets] = await Promise.all([
|
|
107
|
+
this.fetchPrimaryAccount(),
|
|
108
|
+
fetchOrderBookDetails(this.demo)
|
|
109
|
+
]);
|
|
110
|
+
if (!account) {
|
|
111
|
+
return computeMaxBasketSize({
|
|
112
|
+
budgets: {},
|
|
113
|
+
legs: []
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const context = {
|
|
117
|
+
availableBalance: account.available_balance,
|
|
118
|
+
perpBySymbol: toMarketsBySymbol(markets.perp),
|
|
119
|
+
settingsByMarketId: toAccountSettings(markets.perp, account.positions ?? [])
|
|
120
|
+
};
|
|
121
|
+
return computeMaxBasketSize(toLighterBasketCalculation(input.legs, context));
|
|
122
|
+
}
|
|
89
123
|
start() {
|
|
90
124
|
if (this.startInFlight) {
|
|
91
125
|
return this.startInFlight;
|
|
92
126
|
}
|
|
93
|
-
if (this.ws || this.
|
|
127
|
+
if (this.ws || this.positionPollTimer) {
|
|
94
128
|
return Promise.resolve();
|
|
95
129
|
}
|
|
96
130
|
const start = this.performStart().finally(() => {
|
|
@@ -104,16 +138,13 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
104
138
|
/** Creates one tracker lifecycle while repeated callers share the same pending start. */
|
|
105
139
|
async performStart() {
|
|
106
140
|
const lifecycleVersion = ++this.lifecycleVersion;
|
|
107
|
-
const
|
|
108
|
-
fetchOrderBookDetails(this.demo),
|
|
109
|
-
this.fetchAccountTier()
|
|
110
|
-
]);
|
|
141
|
+
const markets = await fetchOrderBookDetails(this.demo);
|
|
111
142
|
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
112
|
-
this.
|
|
113
|
-
this.lastAccountTierFetchAt = Date.now();
|
|
143
|
+
this.spotPriceBySymbol = buildSpotPriceIndex(markets.spot);
|
|
114
144
|
const trackedAssets = /* @__PURE__ */ new Map();
|
|
115
|
-
for (const m of markets) {
|
|
145
|
+
for (const m of markets.perp) {
|
|
116
146
|
this.marketsByMarketId.set(m.market_id, m);
|
|
147
|
+
this.marketsBySymbol.set(m.symbol, m);
|
|
117
148
|
trackedAssets.set(String(m.market_id), mapMarketDefaultAssetInfo(m));
|
|
118
149
|
}
|
|
119
150
|
this.onStateUpdate({
|
|
@@ -131,15 +162,20 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
131
162
|
this.messageListenerId = this.ws.addMessageListener((raw) => this.routeRaw(raw));
|
|
132
163
|
this.openListenerId = this.ws.addOpenListener(() => this.handleWsOpen());
|
|
133
164
|
this.handleWsOpen();
|
|
134
|
-
void this.
|
|
135
|
-
this.
|
|
165
|
+
void this.pollPositions();
|
|
166
|
+
this.positionPollTimer = setInterval(() => void this.pollPositions(), POSITION_POLL_INTERVAL_MS);
|
|
167
|
+
this.marketRefreshTimer = setInterval(() => void this.refreshMarkets(), MARKET_REFRESH_INTERVAL_MS);
|
|
136
168
|
}
|
|
137
169
|
async cleanupConnections() {
|
|
138
170
|
this.lifecycleVersion += 1;
|
|
139
171
|
this.startInFlight = null;
|
|
140
|
-
if (this.
|
|
141
|
-
clearInterval(this.
|
|
142
|
-
this.
|
|
172
|
+
if (this.positionPollTimer) {
|
|
173
|
+
clearInterval(this.positionPollTimer);
|
|
174
|
+
this.positionPollTimer = null;
|
|
175
|
+
}
|
|
176
|
+
if (this.marketRefreshTimer) {
|
|
177
|
+
clearInterval(this.marketRefreshTimer);
|
|
178
|
+
this.marketRefreshTimer = null;
|
|
143
179
|
}
|
|
144
180
|
if (this.messageListenerId) {
|
|
145
181
|
this.ws?.removeMessageListener(this.messageListenerId);
|
|
@@ -163,8 +199,8 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
163
199
|
handleWsOpen() {
|
|
164
200
|
this.stateVersion += 1;
|
|
165
201
|
this.assetsByAssetId.clear();
|
|
166
|
-
this.
|
|
167
|
-
this.
|
|
202
|
+
this.wsPositionsByMarketId.clear();
|
|
203
|
+
this.stats = null;
|
|
168
204
|
this.hasAssets = false;
|
|
169
205
|
this.resetAccountState();
|
|
170
206
|
const trackedAssets = /* @__PURE__ */ new Map();
|
|
@@ -230,20 +266,20 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
230
266
|
...data.positions
|
|
231
267
|
};
|
|
232
268
|
if (isSnapshot) {
|
|
233
|
-
this.
|
|
269
|
+
this.wsPositionsByMarketId.clear();
|
|
234
270
|
}
|
|
235
271
|
for (const [marketId, position] of Object.entries(data.positions)) {
|
|
236
|
-
if (
|
|
237
|
-
const previousPosition = this.
|
|
272
|
+
if (isZeroSize(position.position)) {
|
|
273
|
+
const previousPosition = this.wsPositionsByMarketId.get(marketId);
|
|
238
274
|
if (previousPosition) {
|
|
239
275
|
positionsForMapping[marketId] = {
|
|
240
276
|
...position,
|
|
241
277
|
sign: previousPosition.sign
|
|
242
278
|
};
|
|
243
279
|
}
|
|
244
|
-
this.
|
|
280
|
+
this.wsPositionsByMarketId.delete(marketId);
|
|
245
281
|
} else {
|
|
246
|
-
this.
|
|
282
|
+
this.wsPositionsByMarketId.set(marketId, position);
|
|
247
283
|
}
|
|
248
284
|
}
|
|
249
285
|
const { positions, trackedAssets } = mapPositions(positionsForMapping, !isSnapshot);
|
|
@@ -255,97 +291,89 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
255
291
|
if (balance) update.balance = balance;
|
|
256
292
|
if (Object.keys(update).length > 0) this.onStateUpdate(update);
|
|
257
293
|
}
|
|
294
|
+
/** Keeps both published scopes: cross drives every "Cross" field, total drives the account ones. */
|
|
258
295
|
onUserStats(data) {
|
|
259
296
|
this.stateVersion += 1;
|
|
260
|
-
const
|
|
261
|
-
this.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
availableToTrade: s.available_balance || "0",
|
|
266
|
-
marginUsagePct: s.margin_usage || "0",
|
|
267
|
-
accountType: s.account_trading_mode === 1 ? LighterAccountType.UNIFIED : LighterAccountType.CLASSIC,
|
|
268
|
-
accountTier: this.accountTier
|
|
297
|
+
const stats = data.stats;
|
|
298
|
+
this.stats = {
|
|
299
|
+
accountType: toLighterAccountType(stats.account_trading_mode),
|
|
300
|
+
total: stats.total_stats,
|
|
301
|
+
cross: stats.cross_stats
|
|
269
302
|
};
|
|
270
303
|
const balance = this.buildBalance();
|
|
271
304
|
if (balance) this.onStateUpdate({
|
|
272
305
|
balance
|
|
273
306
|
});
|
|
274
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* The account's balance, built from websocket state alone.
|
|
310
|
+
*
|
|
311
|
+
* Every input is websocket-sourced — `user_stats` for the two account scopes, `account_all` for
|
|
312
|
+
* the assets and positions — except the market context, which is venue-wide metadata this
|
|
313
|
+
* tracker re-reads on its own timer. This is the ONLY producer of the value `getBalance()`
|
|
314
|
+
* returns while the tracker is running; `snapshot()` is the separate REST path and never writes
|
|
315
|
+
* here.
|
|
316
|
+
*
|
|
317
|
+
* Null until both channels have delivered a frame, because a balance built from one of them
|
|
318
|
+
* would be missing half the account.
|
|
319
|
+
*/
|
|
275
320
|
buildBalance() {
|
|
276
|
-
if (!this.
|
|
277
|
-
return
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
321
|
+
if (!this.stats || !this.hasAssets) return null;
|
|
322
|
+
return mapLiveBalance(this.stats, this.assetsByAssetId.values(), this.wsPositionsByMarketId.values(), this.marketContext(), Date.now());
|
|
323
|
+
}
|
|
324
|
+
/** The market data both read paths join account state against, as the latest read left it. */
|
|
325
|
+
marketContext() {
|
|
326
|
+
return {
|
|
327
|
+
perpByMarketId: this.marketsByMarketId,
|
|
328
|
+
spotPriceBySymbol: this.spotPriceBySymbol
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** The account's current per-market settings as the base tracker already holds them. */
|
|
332
|
+
liveAccountSettings() {
|
|
333
|
+
const settings = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const marketId of this.marketsByMarketId.keys()) {
|
|
335
|
+
const info = this.getTrackedAsset(String(marketId));
|
|
336
|
+
if (info) {
|
|
337
|
+
settings.set(String(marketId), info);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return settings;
|
|
281
341
|
}
|
|
282
342
|
async fetchPrimaryAccount() {
|
|
283
343
|
const data = await fetchAccount(Number(this.accountIdentifier), this.demo);
|
|
284
344
|
return data.accounts[0];
|
|
285
345
|
}
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
return void 0;
|
|
289
|
-
}
|
|
290
|
-
try {
|
|
291
|
-
const credentials = this.credentials;
|
|
292
|
-
const accountIndex = Number(this.accountIdentifier);
|
|
293
|
-
const token = await retrieveLighterAuthToken(this.sdk, {
|
|
294
|
-
accountIndex,
|
|
295
|
-
apiKeyIndex: credentials.api_key_index,
|
|
296
|
-
apiPrivateKey: credentials.api_private_key
|
|
297
|
-
}, this.demo);
|
|
298
|
-
const limits = await fetchAccountLimits(accountIndex, token, this.demo);
|
|
299
|
-
return toLighterAccountTier(limits.user_tier);
|
|
300
|
-
} catch (err) {
|
|
301
|
-
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
302
|
-
return void 0;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
pollAccount() {
|
|
306
|
-
if (this.pollInFlight) return this.pollInFlight;
|
|
346
|
+
pollPositions() {
|
|
347
|
+
if (this.positionPollInFlight) return this.positionPollInFlight;
|
|
307
348
|
const lifecycleVersion = this.lifecycleVersion;
|
|
308
349
|
const stateVersion = this.stateVersion;
|
|
309
|
-
const poll = this.
|
|
310
|
-
if (this.
|
|
311
|
-
this.
|
|
350
|
+
const poll = this.performPositionPoll(lifecycleVersion, stateVersion).finally(() => {
|
|
351
|
+
if (this.positionPollInFlight === poll) {
|
|
352
|
+
this.positionPollInFlight = null;
|
|
312
353
|
}
|
|
313
354
|
});
|
|
314
|
-
this.
|
|
355
|
+
this.positionPollInFlight = poll;
|
|
315
356
|
return poll;
|
|
316
357
|
}
|
|
317
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Republishes positions and per-market settings from REST.
|
|
360
|
+
*
|
|
361
|
+
* Deliberately publishes NO balance. Every field of both Lighter variants comes off the two
|
|
362
|
+
* websocket channels — `user_stats` carries both account scopes and `account_all` carries the
|
|
363
|
+
* assets and positions behind them — so a REST balance here would only overwrite a live value
|
|
364
|
+
* with an equal one, while making `getBalance()` REST-derived most of the time.
|
|
365
|
+
*
|
|
366
|
+
* It also leaves `wsPositionsByMarketId` alone. That store feeds the live balance's unrealized
|
|
367
|
+
* PnL and maintenance margin, so writing REST rows into it would make a balance labelled live
|
|
368
|
+
* partly REST-derived without any reader being able to tell.
|
|
369
|
+
*/
|
|
370
|
+
async performPositionPoll(lifecycleVersion, stateVersion) {
|
|
318
371
|
try {
|
|
319
|
-
const
|
|
320
|
-
if (refreshAccountTier) {
|
|
321
|
-
this.lastAccountTierFetchAt = Date.now();
|
|
322
|
-
}
|
|
323
|
-
const [account, refreshedAccountTier] = await Promise.all([
|
|
324
|
-
this.fetchPrimaryAccount(),
|
|
325
|
-
refreshAccountTier ? this.fetchAccountTier() : Promise.resolve(void 0)
|
|
326
|
-
]);
|
|
372
|
+
const account = await this.fetchPrimaryAccount();
|
|
327
373
|
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
328
|
-
if (refreshAccountTier && refreshedAccountTier !== void 0 && refreshedAccountTier !== this.accountTier) {
|
|
329
|
-
this.accountTier = refreshedAccountTier;
|
|
330
|
-
const balance = this.buildBalance();
|
|
331
|
-
if (balance) {
|
|
332
|
-
this.onStateUpdate({
|
|
333
|
-
balance
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
374
|
if (!account || stateVersion !== this.stateVersion) return;
|
|
338
|
-
const
|
|
339
|
-
const { positions, trackedAssets } = mapPositions(positionRecord, true);
|
|
340
|
-
this.stateVersion += 1;
|
|
341
|
-
this.rawPositionsByMarketId.clear();
|
|
342
|
-
for (const [marketId, position] of Object.entries(positionRecord)) {
|
|
343
|
-
if (!new BigNumber(position.position).isZero()) {
|
|
344
|
-
this.rawPositionsByMarketId.set(marketId, position);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
375
|
+
const { positions, trackedAssets } = mapPositions(toPosRecord(account.positions ?? []), true);
|
|
347
376
|
this.onStateUpdate({
|
|
348
|
-
balance: buildAccountBalance(account, this.marketsByMarketId, this.accountTier),
|
|
349
377
|
positions,
|
|
350
378
|
positionsMode: "replace",
|
|
351
379
|
trackedAssets
|
|
@@ -356,9 +384,92 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
356
384
|
}
|
|
357
385
|
}
|
|
358
386
|
}
|
|
359
|
-
|
|
360
|
-
|
|
387
|
+
/** Shares one in-flight market read, so a slow response cannot stack requests behind the WAF. */
|
|
388
|
+
refreshMarkets() {
|
|
389
|
+
if (this.marketRefreshInFlight) return this.marketRefreshInFlight;
|
|
390
|
+
const refresh = this.performMarketRefresh(this.lifecycleVersion).finally(() => {
|
|
391
|
+
if (this.marketRefreshInFlight === refresh) {
|
|
392
|
+
this.marketRefreshInFlight = null;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
this.marketRefreshInFlight = refresh;
|
|
396
|
+
return refresh;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Re-reads the venue's markets and republishes the balance they move.
|
|
400
|
+
*
|
|
401
|
+
* `spotEquity` values each holding at the last trade its own spot book printed, so an index read
|
|
402
|
+
* only at `start()` values a session-long tracker at session-old prices — and every scalar built
|
|
403
|
+
* on it drifts with it.
|
|
404
|
+
*
|
|
405
|
+
* The perp books ride along, which is what closes the gap `toPositionRisks` documents: a market
|
|
406
|
+
* Lighter listed after `start()` was unknown to the balance for the rest of the session, so a
|
|
407
|
+
* position on it left the maintenance figure unknown for just as long. The read already carries
|
|
408
|
+
* them, so learning them costs nothing. Their risk parameters come along as a consequence, which
|
|
409
|
+
* only makes a slowly-moving margin fraction current rather than session-old.
|
|
410
|
+
*
|
|
411
|
+
* Perp books are UPSERTED, never replaced. An account can still hold a position on a market that
|
|
412
|
+
* has dropped out of the listing, and the parameters this tracker last saw price that position —
|
|
413
|
+
* where forgetting the market would stop pricing it at all.
|
|
414
|
+
*
|
|
415
|
+
* A read that fails, or that comes back carrying no spot books at all, leaves the last good price
|
|
416
|
+
* index in place. Stale prices still value the account; an empty index would null every spot
|
|
417
|
+
* figure the account has at once, which is a worse answer than a slightly old one.
|
|
418
|
+
*
|
|
419
|
+
* Only the market data moves here. A newly listed market's exchange-default asset settings reach
|
|
420
|
+
* `trackAsset` consumers on the next websocket epoch, where `handleWsOpen` already republishes
|
|
421
|
+
* them for every known market — publishing them here would overwrite the account's own settings.
|
|
422
|
+
*/
|
|
423
|
+
async performMarketRefresh(lifecycleVersion) {
|
|
424
|
+
try {
|
|
425
|
+
const markets = await fetchOrderBookDetails(this.demo);
|
|
426
|
+
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
427
|
+
for (const market of markets.perp) {
|
|
428
|
+
this.marketsByMarketId.set(market.market_id, market);
|
|
429
|
+
this.marketsBySymbol.set(market.symbol, market);
|
|
430
|
+
}
|
|
431
|
+
if (markets.spot.length > 0) {
|
|
432
|
+
this.spotPriceBySymbol = buildSpotPriceIndex(markets.spot);
|
|
433
|
+
}
|
|
434
|
+
const balance = this.buildBalance();
|
|
435
|
+
if (balance) this.onStateUpdate({
|
|
436
|
+
balance
|
|
437
|
+
});
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (lifecycleVersion === this.lifecycleVersion) {
|
|
440
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function toMarketContext(perpMarkets, spotMarkets) {
|
|
446
|
+
return {
|
|
447
|
+
perpByMarketId: new Map(perpMarkets.map((market) => [
|
|
448
|
+
market.market_id,
|
|
449
|
+
market
|
|
450
|
+
])),
|
|
451
|
+
spotPriceBySymbol: buildSpotPriceIndex(spotMarkets)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
__name(toMarketContext, "toMarketContext");
|
|
455
|
+
function toMarketsBySymbol(perpMarkets) {
|
|
456
|
+
return new Map(perpMarkets.map((market) => [
|
|
457
|
+
market.symbol,
|
|
458
|
+
market
|
|
459
|
+
]));
|
|
460
|
+
}
|
|
461
|
+
__name(toMarketsBySymbol, "toMarketsBySymbol");
|
|
462
|
+
function toAccountSettings(perpMarkets, positions) {
|
|
463
|
+
const settings = /* @__PURE__ */ new Map();
|
|
464
|
+
for (const market of perpMarkets) {
|
|
465
|
+
settings.set(String(market.market_id), mapMarketDefaultAssetInfo(market));
|
|
466
|
+
}
|
|
467
|
+
const { trackedAssets } = mapPositions(toPosRecord(positions), true);
|
|
468
|
+
for (const [marketId, info] of trackedAssets) {
|
|
469
|
+
settings.set(marketId, info);
|
|
361
470
|
}
|
|
471
|
+
return settings;
|
|
362
472
|
}
|
|
473
|
+
__name(toAccountSettings, "toAccountSettings");
|
|
363
474
|
|
|
364
475
|
export { LighterAccountTracker };
|
|
@@ -4,11 +4,5 @@ var LighterAccountType = /* @__PURE__ */ (function(LighterAccountType2) {
|
|
|
4
4
|
LighterAccountType2["UNIFIED"] = "unified";
|
|
5
5
|
return LighterAccountType2;
|
|
6
6
|
})({});
|
|
7
|
-
var LighterAccountTier = /* @__PURE__ */ (function(LighterAccountTier2) {
|
|
8
|
-
LighterAccountTier2["STANDARD"] = "standard";
|
|
9
|
-
LighterAccountTier2["PREMIUM"] = "premium";
|
|
10
|
-
LighterAccountTier2["PLUS"] = "plus";
|
|
11
|
-
return LighterAccountTier2;
|
|
12
|
-
})({});
|
|
13
7
|
|
|
14
|
-
export {
|
|
8
|
+
export { LighterAccountType };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AccountBalanceCore } from '../balance-core';
|
|
3
|
+
|
|
4
|
+
const OkxSpotBalance = AccountBalanceCore.extend({
|
|
5
|
+
accountMode: z.literal("okx:spot")
|
|
6
|
+
});
|
|
7
|
+
const OkxFuturesBalance = AccountBalanceCore.extend({
|
|
8
|
+
accountMode: z.literal("okx:futures")
|
|
9
|
+
});
|
|
10
|
+
const OkxMultiCurrencyMarginBalance = AccountBalanceCore.extend({
|
|
11
|
+
accountMode: z.literal("okx:multiCurrencyMargin")
|
|
12
|
+
});
|
|
13
|
+
const OkxPortfolioMarginBalance = AccountBalanceCore.extend({
|
|
14
|
+
accountMode: z.literal("okx:portfolioMargin")
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export { OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance };
|