@quartz-labs/sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +17 -0
- package/package.json +44 -0
- package/src/client.ts +117 -0
- package/src/config/constants.ts +10 -0
- package/src/helpers.ts +33 -0
- package/src/idl/quartz.json +646 -0
- package/src/index.ts +4 -0
- package/src/model/driftUser.ts +1056 -0
- package/src/types/quartz.ts +1293 -0
- package/src/user.ts +85 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
import { AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_EXP, BN, calculateAssetWeight, calculateLiabilityWeight, calculateLiveOracleTwap, calculateMarketMarginRatio, calculateMarketOpenBidAsk, calculatePerpLiabilityValue, calculatePositionPNL, calculateUnrealizedAssetWeight, calculateUnsettledFundingPnl, calculateWithdrawLimit, calculateWorstCasePerpLiabilityValue, DriftClient, fetchUserAccountsUsingKeys, FIVE_MINUTE, getSignedTokenAmount, getStrictTokenValue, getTokenAmount, getWorstCaseTokenAmounts, isSpotPositionAvailable, isVariant, MARGIN_PRECISION, MarginCategory, ONE, OPEN_ORDER_MARGIN_REQUIREMENT, PerpPosition, PRICE_PRECISION, QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, SPOT_MARKET_WEIGHT_PRECISION, SpotBalanceType, StrictOraclePrice, UserAccount, UserStatus, ZERO, TEN, divCeil, SpotMarketAccount } from "@drift-labs/sdk";
|
|
2
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
3
|
+
import { getDriftUser } from "../helpers.js";
|
|
4
|
+
|
|
5
|
+
export class DriftUser {
|
|
6
|
+
private isInitialized: boolean = false;
|
|
7
|
+
private authority: PublicKey;
|
|
8
|
+
private connection: Connection;
|
|
9
|
+
private driftClient: DriftClient;
|
|
10
|
+
|
|
11
|
+
private userAccount: UserAccount | undefined;
|
|
12
|
+
|
|
13
|
+
constructor (
|
|
14
|
+
authority: PublicKey,
|
|
15
|
+
connection: Connection,
|
|
16
|
+
driftClient: DriftClient,
|
|
17
|
+
userAccount?: UserAccount
|
|
18
|
+
) {
|
|
19
|
+
this.authority = authority;
|
|
20
|
+
this.connection = connection;
|
|
21
|
+
this.driftClient = driftClient;
|
|
22
|
+
|
|
23
|
+
if (userAccount) {
|
|
24
|
+
this.userAccount = userAccount;
|
|
25
|
+
this.isInitialized = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async initialize(): Promise<void> {
|
|
30
|
+
if (this.isInitialized) return;
|
|
31
|
+
|
|
32
|
+
const [ userAccount ] = await fetchUserAccountsUsingKeys(
|
|
33
|
+
this.connection,
|
|
34
|
+
this.driftClient.program,
|
|
35
|
+
[getDriftUser(this.authority)]
|
|
36
|
+
);
|
|
37
|
+
if (!userAccount) throw new Error("Drift user not found");
|
|
38
|
+
this.userAccount = userAccount;
|
|
39
|
+
this.isInitialized = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public getHealth(): number{
|
|
43
|
+
if (!this.isInitialized) throw new Error("DriftUser not initialized");
|
|
44
|
+
|
|
45
|
+
if (this.isBeingLiquidated()) return 0;
|
|
46
|
+
|
|
47
|
+
const totalCollateral = this.getTotalCollateral('Maintenance');
|
|
48
|
+
const maintenanceMarginReq = this.getMaintenanceMarginRequirement();
|
|
49
|
+
|
|
50
|
+
if (maintenanceMarginReq.eq(ZERO) && totalCollateral.gte(ZERO)) {
|
|
51
|
+
return 100;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (totalCollateral.lte(ZERO)) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Math.round(
|
|
59
|
+
Math.min(
|
|
60
|
+
100,
|
|
61
|
+
Math.max(
|
|
62
|
+
0,
|
|
63
|
+
(1 - maintenanceMarginReq.toNumber() / totalCollateral.toNumber()) * 100
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public getTokenAmount(marketIndex: number): BN {
|
|
70
|
+
if (!this.isInitialized) throw new Error("DriftUser not initialized");
|
|
71
|
+
|
|
72
|
+
const spotPosition = this.userAccount!.spotPositions.find(
|
|
73
|
+
(position) => position.marketIndex === marketIndex
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (spotPosition === undefined) {
|
|
77
|
+
return ZERO;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex)!;
|
|
81
|
+
return getSignedTokenAmount(
|
|
82
|
+
getTokenAmount(
|
|
83
|
+
spotPosition.scaledBalance,
|
|
84
|
+
spotMarket,
|
|
85
|
+
spotPosition.balanceType
|
|
86
|
+
),
|
|
87
|
+
spotPosition.balanceType
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public getWithdrawalLimit(marketIndex: number, reduceOnly?: boolean): BN {
|
|
92
|
+
const nowTs = new BN(Math.floor(Date.now() / 1000));
|
|
93
|
+
const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
|
|
94
|
+
|
|
95
|
+
// eslint-disable-next-line prefer-const
|
|
96
|
+
let { borrowLimit, withdrawLimit } = calculateWithdrawLimit(
|
|
97
|
+
spotMarket!,
|
|
98
|
+
nowTs
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const freeCollateral = this.getFreeCollateral();
|
|
102
|
+
const initialMarginRequirement = this.getMarginRequirement('Initial', undefined, false);
|
|
103
|
+
const oracleData = this.driftClient.getOracleDataForSpotMarket(marketIndex);
|
|
104
|
+
const precisionIncrease = TEN.pow(new BN(spotMarket!.decimals - 6));
|
|
105
|
+
|
|
106
|
+
const { canBypass, depositAmount: userDepositAmount } =
|
|
107
|
+
this.canBypassWithdrawLimits(marketIndex);
|
|
108
|
+
if (canBypass) {
|
|
109
|
+
withdrawLimit = BN.max(withdrawLimit, userDepositAmount);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const assetWeight = calculateAssetWeight(
|
|
113
|
+
userDepositAmount,
|
|
114
|
+
oracleData.price,
|
|
115
|
+
spotMarket!,
|
|
116
|
+
'Initial'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let amountWithdrawable;
|
|
120
|
+
if (assetWeight.eq(ZERO)) {
|
|
121
|
+
amountWithdrawable = userDepositAmount;
|
|
122
|
+
} else if (initialMarginRequirement.eq(ZERO)) {
|
|
123
|
+
amountWithdrawable = userDepositAmount;
|
|
124
|
+
} else {
|
|
125
|
+
amountWithdrawable = divCeil(
|
|
126
|
+
divCeil(freeCollateral.mul(MARGIN_PRECISION), assetWeight).mul(
|
|
127
|
+
PRICE_PRECISION
|
|
128
|
+
),
|
|
129
|
+
oracleData.price
|
|
130
|
+
).mul(precisionIncrease);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const maxWithdrawValue = BN.min(
|
|
134
|
+
BN.min(amountWithdrawable, userDepositAmount),
|
|
135
|
+
withdrawLimit.abs()
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (reduceOnly) {
|
|
139
|
+
return BN.max(maxWithdrawValue, ZERO);
|
|
140
|
+
} else {
|
|
141
|
+
const weightedAssetValue = this.getSpotMarketAssetValue(
|
|
142
|
+
'Initial',
|
|
143
|
+
marketIndex,
|
|
144
|
+
false
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const freeCollatAfterWithdraw = userDepositAmount.gt(ZERO)
|
|
148
|
+
? freeCollateral.sub(weightedAssetValue)
|
|
149
|
+
: freeCollateral;
|
|
150
|
+
|
|
151
|
+
const maxLiabilityAllowed = freeCollatAfterWithdraw
|
|
152
|
+
.mul(MARGIN_PRECISION)
|
|
153
|
+
.div(new BN(spotMarket!.initialLiabilityWeight))
|
|
154
|
+
.mul(PRICE_PRECISION)
|
|
155
|
+
.div(oracleData.price)
|
|
156
|
+
.mul(precisionIncrease);
|
|
157
|
+
|
|
158
|
+
const maxBorrowValue = BN.min(
|
|
159
|
+
maxWithdrawValue.add(maxLiabilityAllowed),
|
|
160
|
+
borrowLimit.abs()
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return BN.max(maxBorrowValue, ZERO);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private getFreeCollateral(marginCategory: MarginCategory = 'Initial'): BN {
|
|
168
|
+
const totalCollateral = this.getTotalCollateral(marginCategory, true);
|
|
169
|
+
const marginRequirement =
|
|
170
|
+
marginCategory === 'Initial'
|
|
171
|
+
? this.getMarginRequirement('Initial', undefined, false)
|
|
172
|
+
: this.getMaintenanceMarginRequirement();
|
|
173
|
+
const freeCollateral = totalCollateral.sub(marginRequirement);
|
|
174
|
+
return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private canBypassWithdrawLimits(marketIndex: number): {
|
|
178
|
+
canBypass: boolean;
|
|
179
|
+
netDeposits: BN;
|
|
180
|
+
depositAmount: BN;
|
|
181
|
+
maxDepositAmount: BN;
|
|
182
|
+
} {
|
|
183
|
+
const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
|
|
184
|
+
const maxDepositAmount = spotMarket!.withdrawGuardThreshold.div(new BN(10));
|
|
185
|
+
const position = this.userAccount!.spotPositions.find((position) => position.marketIndex === marketIndex);
|
|
186
|
+
|
|
187
|
+
const netDeposits = this.userAccount!.totalDeposits.sub(
|
|
188
|
+
this.userAccount!.totalWithdraws
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!position) {
|
|
192
|
+
return {
|
|
193
|
+
canBypass: false,
|
|
194
|
+
maxDepositAmount,
|
|
195
|
+
depositAmount: ZERO,
|
|
196
|
+
netDeposits,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (isVariant(position.balanceType, 'borrow')) {
|
|
201
|
+
return {
|
|
202
|
+
canBypass: false,
|
|
203
|
+
maxDepositAmount,
|
|
204
|
+
netDeposits,
|
|
205
|
+
depositAmount: ZERO,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const depositAmount = getTokenAmount(
|
|
210
|
+
position.scaledBalance,
|
|
211
|
+
spotMarket!,
|
|
212
|
+
SpotBalanceType.DEPOSIT
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (netDeposits.lt(ZERO)) {
|
|
216
|
+
return {
|
|
217
|
+
canBypass: false,
|
|
218
|
+
maxDepositAmount,
|
|
219
|
+
depositAmount,
|
|
220
|
+
netDeposits,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
canBypass: depositAmount.lt(maxDepositAmount),
|
|
226
|
+
maxDepositAmount,
|
|
227
|
+
netDeposits,
|
|
228
|
+
depositAmount,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private isBeingLiquidated(): boolean {
|
|
233
|
+
return (
|
|
234
|
+
(this.userAccount!.status &
|
|
235
|
+
(UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) >
|
|
236
|
+
0
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public getTotalCollateral(
|
|
241
|
+
marginCategory: MarginCategory = 'Initial',
|
|
242
|
+
strict = false,
|
|
243
|
+
includeOpenOrders = true
|
|
244
|
+
): BN {
|
|
245
|
+
if (!this.isInitialized) throw new Error("DriftUser not initialized");
|
|
246
|
+
|
|
247
|
+
return this.getSpotMarketAssetValue(
|
|
248
|
+
marginCategory,
|
|
249
|
+
undefined,
|
|
250
|
+
includeOpenOrders,
|
|
251
|
+
strict
|
|
252
|
+
).add(this.getUnrealizedPNL(true, undefined, marginCategory, strict));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private getSpotMarketAssetValue(
|
|
256
|
+
marginCategory: MarginCategory,
|
|
257
|
+
marketIndex?: number,
|
|
258
|
+
includeOpenOrders?: boolean,
|
|
259
|
+
strict = false,
|
|
260
|
+
now?: BN
|
|
261
|
+
): BN {
|
|
262
|
+
const { totalAssetValue } = this.getSpotMarketAssetAndLiabilityValue(
|
|
263
|
+
marginCategory,
|
|
264
|
+
marketIndex,
|
|
265
|
+
undefined,
|
|
266
|
+
includeOpenOrders,
|
|
267
|
+
strict,
|
|
268
|
+
now
|
|
269
|
+
);
|
|
270
|
+
return totalAssetValue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getSpotMarketAssetAndLiabilityValue(
|
|
274
|
+
marginCategory: MarginCategory,
|
|
275
|
+
marketIndex?: number,
|
|
276
|
+
liquidationBuffer?: BN,
|
|
277
|
+
includeOpenOrders?: boolean,
|
|
278
|
+
strict = false,
|
|
279
|
+
now?: BN
|
|
280
|
+
): { totalAssetValue: BN; totalLiabilityValue: BN } {
|
|
281
|
+
now = now || new BN(new Date().getTime() / 1000);
|
|
282
|
+
let netQuoteValue = ZERO;
|
|
283
|
+
let totalAssetValue = ZERO;
|
|
284
|
+
let totalLiabilityValue = ZERO;
|
|
285
|
+
for (const spotPosition of this.userAccount!.spotPositions) {
|
|
286
|
+
const countForBase =
|
|
287
|
+
marketIndex === undefined || spotPosition.marketIndex === marketIndex;
|
|
288
|
+
|
|
289
|
+
const countForQuote =
|
|
290
|
+
marketIndex === undefined ||
|
|
291
|
+
marketIndex === QUOTE_SPOT_MARKET_INDEX ||
|
|
292
|
+
(includeOpenOrders && spotPosition.openOrders !== 0);
|
|
293
|
+
if (
|
|
294
|
+
isSpotPositionAvailable(spotPosition) ||
|
|
295
|
+
(!countForBase && !countForQuote)
|
|
296
|
+
) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const spotMarketAccount: SpotMarketAccount =
|
|
301
|
+
this.driftClient.getSpotMarketAccount(spotPosition.marketIndex)!;
|
|
302
|
+
|
|
303
|
+
const oraclePriceData = this.driftClient.getOracleDataForSpotMarket(
|
|
304
|
+
spotPosition.marketIndex
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
let twap5min;
|
|
308
|
+
if (strict) {
|
|
309
|
+
twap5min = calculateLiveOracleTwap(
|
|
310
|
+
spotMarketAccount.historicalOracleData,
|
|
311
|
+
oraclePriceData,
|
|
312
|
+
now,
|
|
313
|
+
FIVE_MINUTE // 5MIN
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const strictOraclePrice = new StrictOraclePrice(
|
|
317
|
+
oraclePriceData.price,
|
|
318
|
+
twap5min
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX &&
|
|
323
|
+
countForQuote
|
|
324
|
+
) {
|
|
325
|
+
const tokenAmount = getSignedTokenAmount(
|
|
326
|
+
getTokenAmount(
|
|
327
|
+
spotPosition.scaledBalance,
|
|
328
|
+
spotMarketAccount,
|
|
329
|
+
spotPosition.balanceType
|
|
330
|
+
),
|
|
331
|
+
spotPosition.balanceType
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (isVariant(spotPosition.balanceType, 'borrow')) {
|
|
335
|
+
const weightedTokenValue = this.getSpotLiabilityValue(
|
|
336
|
+
tokenAmount,
|
|
337
|
+
strictOraclePrice,
|
|
338
|
+
spotMarketAccount,
|
|
339
|
+
marginCategory,
|
|
340
|
+
liquidationBuffer
|
|
341
|
+
).abs();
|
|
342
|
+
|
|
343
|
+
netQuoteValue = netQuoteValue.sub(weightedTokenValue);
|
|
344
|
+
} else {
|
|
345
|
+
const weightedTokenValue = this.getSpotAssetValue(
|
|
346
|
+
tokenAmount,
|
|
347
|
+
strictOraclePrice,
|
|
348
|
+
spotMarketAccount,
|
|
349
|
+
marginCategory
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
netQuoteValue = netQuoteValue.add(weightedTokenValue);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!includeOpenOrders && countForBase) {
|
|
359
|
+
if (isVariant(spotPosition.balanceType, 'borrow')) {
|
|
360
|
+
const tokenAmount = getSignedTokenAmount(
|
|
361
|
+
getTokenAmount(
|
|
362
|
+
spotPosition.scaledBalance,
|
|
363
|
+
spotMarketAccount,
|
|
364
|
+
spotPosition.balanceType
|
|
365
|
+
),
|
|
366
|
+
SpotBalanceType.BORROW
|
|
367
|
+
);
|
|
368
|
+
const liabilityValue = this.getSpotLiabilityValue(
|
|
369
|
+
tokenAmount,
|
|
370
|
+
strictOraclePrice,
|
|
371
|
+
spotMarketAccount,
|
|
372
|
+
marginCategory,
|
|
373
|
+
liquidationBuffer
|
|
374
|
+
).abs();
|
|
375
|
+
totalLiabilityValue = totalLiabilityValue.add(liabilityValue);
|
|
376
|
+
|
|
377
|
+
continue;
|
|
378
|
+
} else {
|
|
379
|
+
const tokenAmount = getTokenAmount(
|
|
380
|
+
spotPosition.scaledBalance,
|
|
381
|
+
spotMarketAccount,
|
|
382
|
+
spotPosition.balanceType
|
|
383
|
+
);
|
|
384
|
+
const assetValue = this.getSpotAssetValue(
|
|
385
|
+
tokenAmount,
|
|
386
|
+
strictOraclePrice,
|
|
387
|
+
spotMarketAccount,
|
|
388
|
+
marginCategory
|
|
389
|
+
);
|
|
390
|
+
totalAssetValue = totalAssetValue.add(assetValue);
|
|
391
|
+
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const {
|
|
397
|
+
tokenAmount: worstCaseTokenAmount,
|
|
398
|
+
ordersValue: worstCaseQuoteTokenAmount,
|
|
399
|
+
} = getWorstCaseTokenAmounts(
|
|
400
|
+
spotPosition,
|
|
401
|
+
spotMarketAccount,
|
|
402
|
+
strictOraclePrice,
|
|
403
|
+
marginCategory,
|
|
404
|
+
this.userAccount!.maxMarginRatio
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
if (worstCaseTokenAmount.gt(ZERO) && countForBase) {
|
|
408
|
+
const baseAssetValue = this.getSpotAssetValue(
|
|
409
|
+
worstCaseTokenAmount,
|
|
410
|
+
strictOraclePrice,
|
|
411
|
+
spotMarketAccount,
|
|
412
|
+
marginCategory
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
totalAssetValue = totalAssetValue.add(baseAssetValue);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (worstCaseTokenAmount.lt(ZERO) && countForBase) {
|
|
419
|
+
const baseLiabilityValue = this.getSpotLiabilityValue(
|
|
420
|
+
worstCaseTokenAmount,
|
|
421
|
+
strictOraclePrice,
|
|
422
|
+
spotMarketAccount,
|
|
423
|
+
marginCategory,
|
|
424
|
+
liquidationBuffer
|
|
425
|
+
).abs();
|
|
426
|
+
|
|
427
|
+
totalLiabilityValue = totalLiabilityValue.add(baseLiabilityValue);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (worstCaseQuoteTokenAmount.gt(ZERO) && countForQuote) {
|
|
431
|
+
netQuoteValue = netQuoteValue.add(worstCaseQuoteTokenAmount);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (worstCaseQuoteTokenAmount.lt(ZERO) && countForQuote) {
|
|
435
|
+
let weight = SPOT_MARKET_WEIGHT_PRECISION;
|
|
436
|
+
if (marginCategory === 'Initial') {
|
|
437
|
+
weight = BN.max(weight, new BN(this.userAccount!.maxMarginRatio));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const weightedTokenValue = worstCaseQuoteTokenAmount
|
|
441
|
+
.abs()
|
|
442
|
+
.mul(weight)
|
|
443
|
+
.div(SPOT_MARKET_WEIGHT_PRECISION);
|
|
444
|
+
|
|
445
|
+
netQuoteValue = netQuoteValue.sub(weightedTokenValue);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
totalLiabilityValue = totalLiabilityValue.add(
|
|
449
|
+
new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (marketIndex === undefined || marketIndex === QUOTE_SPOT_MARKET_INDEX) {
|
|
454
|
+
if (netQuoteValue.gt(ZERO)) {
|
|
455
|
+
totalAssetValue = totalAssetValue.add(netQuoteValue);
|
|
456
|
+
} else {
|
|
457
|
+
totalLiabilityValue = totalLiabilityValue.add(netQuoteValue.abs());
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return { totalAssetValue, totalLiabilityValue };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private getSpotLiabilityValue(
|
|
465
|
+
tokenAmount: BN,
|
|
466
|
+
strictOraclePrice: StrictOraclePrice,
|
|
467
|
+
spotMarketAccount: SpotMarketAccount,
|
|
468
|
+
marginCategory?: MarginCategory,
|
|
469
|
+
liquidationBuffer?: BN
|
|
470
|
+
): BN {
|
|
471
|
+
let liabilityValue = getStrictTokenValue(
|
|
472
|
+
tokenAmount,
|
|
473
|
+
spotMarketAccount.decimals,
|
|
474
|
+
strictOraclePrice
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
if (marginCategory !== undefined) {
|
|
478
|
+
let weight = calculateLiabilityWeight(
|
|
479
|
+
tokenAmount,
|
|
480
|
+
spotMarketAccount,
|
|
481
|
+
marginCategory
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
if (
|
|
485
|
+
marginCategory === 'Initial' &&
|
|
486
|
+
spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX
|
|
487
|
+
) {
|
|
488
|
+
weight = BN.max(
|
|
489
|
+
weight,
|
|
490
|
+
SPOT_MARKET_WEIGHT_PRECISION.addn(
|
|
491
|
+
this.userAccount!.maxMarginRatio
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (liquidationBuffer !== undefined) {
|
|
497
|
+
weight = weight.add(liquidationBuffer);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
liabilityValue = liabilityValue
|
|
501
|
+
.mul(weight)
|
|
502
|
+
.div(SPOT_MARKET_WEIGHT_PRECISION);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return liabilityValue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private getSpotAssetValue(
|
|
509
|
+
tokenAmount: BN,
|
|
510
|
+
strictOraclePrice: StrictOraclePrice,
|
|
511
|
+
spotMarketAccount: SpotMarketAccount,
|
|
512
|
+
marginCategory?: MarginCategory
|
|
513
|
+
): BN {
|
|
514
|
+
let assetValue = getStrictTokenValue(
|
|
515
|
+
tokenAmount,
|
|
516
|
+
spotMarketAccount.decimals,
|
|
517
|
+
strictOraclePrice
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
if (marginCategory !== undefined) {
|
|
521
|
+
let weight = calculateAssetWeight(
|
|
522
|
+
tokenAmount,
|
|
523
|
+
strictOraclePrice.current,
|
|
524
|
+
spotMarketAccount,
|
|
525
|
+
marginCategory
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
if (
|
|
529
|
+
marginCategory === 'Initial' &&
|
|
530
|
+
spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX
|
|
531
|
+
) {
|
|
532
|
+
const userCustomAssetWeight = BN.max(
|
|
533
|
+
ZERO,
|
|
534
|
+
SPOT_MARKET_WEIGHT_PRECISION.subn(
|
|
535
|
+
this.userAccount!.maxMarginRatio
|
|
536
|
+
)
|
|
537
|
+
);
|
|
538
|
+
weight = BN.min(weight, userCustomAssetWeight);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
assetValue = assetValue.mul(weight).div(SPOT_MARKET_WEIGHT_PRECISION);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return assetValue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private getUnrealizedPNL(
|
|
548
|
+
withFunding: boolean,
|
|
549
|
+
marketIndex?: number,
|
|
550
|
+
withWeightMarginCategory?: MarginCategory,
|
|
551
|
+
strict = false
|
|
552
|
+
): BN {
|
|
553
|
+
return this.getActivePerpPositions()
|
|
554
|
+
.filter((pos) =>
|
|
555
|
+
marketIndex !== undefined ? pos.marketIndex === marketIndex : true
|
|
556
|
+
)
|
|
557
|
+
.reduce((unrealizedPnl, perpPosition) => {
|
|
558
|
+
const market = this.driftClient.getPerpMarketAccount(
|
|
559
|
+
perpPosition.marketIndex
|
|
560
|
+
)!;
|
|
561
|
+
const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(
|
|
562
|
+
market.marketIndex
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
|
|
566
|
+
market.quoteSpotMarketIndex
|
|
567
|
+
)!;
|
|
568
|
+
const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(
|
|
569
|
+
market.quoteSpotMarketIndex
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (perpPosition.lpShares.gt(ZERO)) {
|
|
573
|
+
perpPosition = this.getPerpPositionWithLPSettle(
|
|
574
|
+
perpPosition.marketIndex,
|
|
575
|
+
undefined,
|
|
576
|
+
!!withWeightMarginCategory
|
|
577
|
+
)[0];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let positionUnrealizedPnl = calculatePositionPNL(
|
|
581
|
+
market,
|
|
582
|
+
perpPosition,
|
|
583
|
+
withFunding,
|
|
584
|
+
oraclePriceData
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
let quotePrice;
|
|
588
|
+
if (strict && positionUnrealizedPnl.gt(ZERO)) {
|
|
589
|
+
quotePrice = BN.min(
|
|
590
|
+
quoteOraclePriceData.price,
|
|
591
|
+
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
|
|
592
|
+
);
|
|
593
|
+
} else if (strict && positionUnrealizedPnl.lt(ZERO)) {
|
|
594
|
+
quotePrice = BN.max(
|
|
595
|
+
quoteOraclePriceData.price,
|
|
596
|
+
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
|
|
597
|
+
);
|
|
598
|
+
} else {
|
|
599
|
+
quotePrice = quoteOraclePriceData.price;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
positionUnrealizedPnl = positionUnrealizedPnl
|
|
603
|
+
.mul(quotePrice)
|
|
604
|
+
.div(PRICE_PRECISION);
|
|
605
|
+
|
|
606
|
+
if (withWeightMarginCategory !== undefined) {
|
|
607
|
+
if (positionUnrealizedPnl.gt(ZERO)) {
|
|
608
|
+
positionUnrealizedPnl = positionUnrealizedPnl
|
|
609
|
+
.mul(
|
|
610
|
+
calculateUnrealizedAssetWeight(
|
|
611
|
+
market,
|
|
612
|
+
quoteSpotMarket,
|
|
613
|
+
positionUnrealizedPnl,
|
|
614
|
+
withWeightMarginCategory,
|
|
615
|
+
oraclePriceData
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
.div(new BN(SPOT_MARKET_WEIGHT_PRECISION));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return unrealizedPnl.add(positionUnrealizedPnl);
|
|
623
|
+
}, ZERO);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private getActivePerpPositions() {
|
|
627
|
+
return this.userAccount!.perpPositions.filter(
|
|
628
|
+
(pos) =>
|
|
629
|
+
!pos.baseAssetAmount.eq(ZERO) ||
|
|
630
|
+
!pos.quoteAssetAmount.eq(ZERO) ||
|
|
631
|
+
!(pos.openOrders == 0) ||
|
|
632
|
+
!pos.lpShares.eq(ZERO)
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private getPerpPositionWithLPSettle(
|
|
637
|
+
marketIndex: number,
|
|
638
|
+
originalPosition?: PerpPosition,
|
|
639
|
+
burnLpShares = false,
|
|
640
|
+
includeRemainderInBaseAmount = false
|
|
641
|
+
): [PerpPosition, BN, BN] {
|
|
642
|
+
originalPosition =
|
|
643
|
+
originalPosition ??
|
|
644
|
+
this.getPerpPosition(marketIndex) ??
|
|
645
|
+
this.getEmptyPosition(marketIndex);
|
|
646
|
+
|
|
647
|
+
if (originalPosition.lpShares.eq(ZERO)) {
|
|
648
|
+
return [originalPosition, ZERO, ZERO];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const position = this.getClonedPosition(originalPosition);
|
|
652
|
+
const market = this.driftClient.getPerpMarketAccount(position.marketIndex)!;
|
|
653
|
+
|
|
654
|
+
if (market.amm.perLpBase != position.perLpBase) {
|
|
655
|
+
// perLpBase = 1 => per 10 LP shares, perLpBase = -1 => per 0.1 LP shares
|
|
656
|
+
const expoDiff = market.amm.perLpBase - position.perLpBase;
|
|
657
|
+
const marketPerLpRebaseScalar = new BN(10 ** Math.abs(expoDiff));
|
|
658
|
+
|
|
659
|
+
if (expoDiff > 0) {
|
|
660
|
+
position.lastBaseAssetAmountPerLp =
|
|
661
|
+
position.lastBaseAssetAmountPerLp.mul(marketPerLpRebaseScalar);
|
|
662
|
+
position.lastQuoteAssetAmountPerLp =
|
|
663
|
+
position.lastQuoteAssetAmountPerLp.mul(marketPerLpRebaseScalar);
|
|
664
|
+
} else {
|
|
665
|
+
position.lastBaseAssetAmountPerLp =
|
|
666
|
+
position.lastBaseAssetAmountPerLp.div(marketPerLpRebaseScalar);
|
|
667
|
+
position.lastQuoteAssetAmountPerLp =
|
|
668
|
+
position.lastQuoteAssetAmountPerLp.div(marketPerLpRebaseScalar);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
position.perLpBase = position.perLpBase + expoDiff;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const nShares = position.lpShares;
|
|
675
|
+
|
|
676
|
+
// incorp unsettled funding on pre settled position
|
|
677
|
+
const quoteFundingPnl = calculateUnsettledFundingPnl(market, position);
|
|
678
|
+
|
|
679
|
+
let baseUnit = AMM_RESERVE_PRECISION;
|
|
680
|
+
if (market.amm.perLpBase == position.perLpBase) {
|
|
681
|
+
if (
|
|
682
|
+
position.perLpBase >= 0 &&
|
|
683
|
+
position.perLpBase <= AMM_RESERVE_PRECISION_EXP.toNumber()
|
|
684
|
+
) {
|
|
685
|
+
const marketPerLpRebase = new BN(10 ** market.amm.perLpBase);
|
|
686
|
+
baseUnit = baseUnit.mul(marketPerLpRebase);
|
|
687
|
+
} else if (
|
|
688
|
+
position.perLpBase < 0 &&
|
|
689
|
+
position.perLpBase >= -AMM_RESERVE_PRECISION_EXP.toNumber()
|
|
690
|
+
) {
|
|
691
|
+
const marketPerLpRebase = new BN(10 ** Math.abs(market.amm.perLpBase));
|
|
692
|
+
baseUnit = baseUnit.div(marketPerLpRebase);
|
|
693
|
+
} else {
|
|
694
|
+
throw 'cannot calc';
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
throw 'market.amm.perLpBase != position.perLpBase';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const deltaBaa = market.amm.baseAssetAmountPerLp
|
|
701
|
+
.sub(position.lastBaseAssetAmountPerLp)
|
|
702
|
+
.mul(nShares)
|
|
703
|
+
.div(baseUnit);
|
|
704
|
+
const deltaQaa = market.amm.quoteAssetAmountPerLp
|
|
705
|
+
.sub(position.lastQuoteAssetAmountPerLp)
|
|
706
|
+
.mul(nShares)
|
|
707
|
+
.div(baseUnit);
|
|
708
|
+
|
|
709
|
+
function sign(v: BN) {
|
|
710
|
+
return v.isNeg() ? new BN(-1) : new BN(1);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function standardize(amount: BN, stepSize: BN) {
|
|
714
|
+
const remainder = amount.abs().mod(stepSize).mul(sign(amount));
|
|
715
|
+
const standardizedAmount = amount.sub(remainder);
|
|
716
|
+
return [standardizedAmount, remainder];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const [standardizedBaa, remainderBaa] = standardize(
|
|
720
|
+
deltaBaa,
|
|
721
|
+
market.amm.orderStepSize
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
position.remainderBaseAssetAmount += remainderBaa.toNumber();
|
|
725
|
+
|
|
726
|
+
if (
|
|
727
|
+
Math.abs(position.remainderBaseAssetAmount) >
|
|
728
|
+
market.amm.orderStepSize.toNumber()
|
|
729
|
+
) {
|
|
730
|
+
const [newStandardizedBaa, newRemainderBaa] = standardize(
|
|
731
|
+
new BN(position.remainderBaseAssetAmount),
|
|
732
|
+
market.amm.orderStepSize
|
|
733
|
+
);
|
|
734
|
+
position.baseAssetAmount =
|
|
735
|
+
position.baseAssetAmount.add(newStandardizedBaa);
|
|
736
|
+
position.remainderBaseAssetAmount = newRemainderBaa.toNumber();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let dustBaseAssetValue = ZERO;
|
|
740
|
+
if (burnLpShares && position.remainderBaseAssetAmount != 0) {
|
|
741
|
+
const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(
|
|
742
|
+
position.marketIndex
|
|
743
|
+
);
|
|
744
|
+
dustBaseAssetValue = new BN(Math.abs(position.remainderBaseAssetAmount))
|
|
745
|
+
.mul(oraclePriceData.price)
|
|
746
|
+
.div(AMM_RESERVE_PRECISION)
|
|
747
|
+
.add(ONE);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let updateType;
|
|
751
|
+
if (position.baseAssetAmount.eq(ZERO)) {
|
|
752
|
+
updateType = 'open';
|
|
753
|
+
} else if (sign(position.baseAssetAmount).eq(sign(deltaBaa))) {
|
|
754
|
+
updateType = 'increase';
|
|
755
|
+
} else if (position.baseAssetAmount.abs().gt(deltaBaa.abs())) {
|
|
756
|
+
updateType = 'reduce';
|
|
757
|
+
} else if (position.baseAssetAmount.abs().eq(deltaBaa.abs())) {
|
|
758
|
+
updateType = 'close';
|
|
759
|
+
} else {
|
|
760
|
+
updateType = 'flip';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let newQuoteEntry;
|
|
764
|
+
let pnl;
|
|
765
|
+
if (updateType == 'open' || updateType == 'increase') {
|
|
766
|
+
newQuoteEntry = position.quoteEntryAmount.add(deltaQaa);
|
|
767
|
+
pnl = ZERO;
|
|
768
|
+
} else if (updateType == 'reduce' || updateType == 'close') {
|
|
769
|
+
newQuoteEntry = position.quoteEntryAmount.sub(
|
|
770
|
+
position.quoteEntryAmount
|
|
771
|
+
.mul(deltaBaa.abs())
|
|
772
|
+
.div(position.baseAssetAmount.abs())
|
|
773
|
+
);
|
|
774
|
+
pnl = position.quoteEntryAmount.sub(newQuoteEntry).add(deltaQaa);
|
|
775
|
+
} else {
|
|
776
|
+
newQuoteEntry = deltaQaa.sub(
|
|
777
|
+
deltaQaa.mul(position.baseAssetAmount.abs()).div(deltaBaa.abs())
|
|
778
|
+
);
|
|
779
|
+
pnl = position.quoteEntryAmount.add(deltaQaa.sub(newQuoteEntry));
|
|
780
|
+
}
|
|
781
|
+
position.quoteEntryAmount = newQuoteEntry;
|
|
782
|
+
position.baseAssetAmount = position.baseAssetAmount.add(standardizedBaa);
|
|
783
|
+
position.quoteAssetAmount = position.quoteAssetAmount
|
|
784
|
+
.add(deltaQaa)
|
|
785
|
+
.add(quoteFundingPnl)
|
|
786
|
+
.sub(dustBaseAssetValue);
|
|
787
|
+
position.quoteBreakEvenAmount = position.quoteBreakEvenAmount
|
|
788
|
+
.add(deltaQaa)
|
|
789
|
+
.add(quoteFundingPnl)
|
|
790
|
+
.sub(dustBaseAssetValue);
|
|
791
|
+
|
|
792
|
+
// update open bids/asks
|
|
793
|
+
const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk(
|
|
794
|
+
market.amm.baseAssetReserve,
|
|
795
|
+
market.amm.minBaseAssetReserve,
|
|
796
|
+
market.amm.maxBaseAssetReserve,
|
|
797
|
+
market.amm.orderStepSize
|
|
798
|
+
);
|
|
799
|
+
const lpOpenBids = marketOpenBids
|
|
800
|
+
.mul(position.lpShares)
|
|
801
|
+
.div(market.amm.sqrtK);
|
|
802
|
+
const lpOpenAsks = marketOpenAsks
|
|
803
|
+
.mul(position.lpShares)
|
|
804
|
+
.div(market.amm.sqrtK);
|
|
805
|
+
position.openBids = lpOpenBids.add(position.openBids);
|
|
806
|
+
position.openAsks = lpOpenAsks.add(position.openAsks);
|
|
807
|
+
|
|
808
|
+
// eliminate counting funding on settled position
|
|
809
|
+
if (position.baseAssetAmount.gt(ZERO)) {
|
|
810
|
+
position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong;
|
|
811
|
+
} else if (position.baseAssetAmount.lt(ZERO)) {
|
|
812
|
+
position.lastCumulativeFundingRate =
|
|
813
|
+
market.amm.cumulativeFundingRateShort;
|
|
814
|
+
} else {
|
|
815
|
+
position.lastCumulativeFundingRate = ZERO;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const remainderBeforeRemoval = new BN(position.remainderBaseAssetAmount);
|
|
819
|
+
|
|
820
|
+
if (includeRemainderInBaseAmount) {
|
|
821
|
+
position.baseAssetAmount = position.baseAssetAmount.add(
|
|
822
|
+
remainderBeforeRemoval
|
|
823
|
+
);
|
|
824
|
+
position.remainderBaseAssetAmount = 0;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return [position, remainderBeforeRemoval, pnl];
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private getPerpPosition(marketIndex: number): PerpPosition | undefined {
|
|
831
|
+
const activePositions = this.userAccount!.perpPositions.filter(
|
|
832
|
+
(pos) =>
|
|
833
|
+
!pos.baseAssetAmount.eq(ZERO) ||
|
|
834
|
+
!pos.quoteAssetAmount.eq(ZERO) ||
|
|
835
|
+
!(pos.openOrders == 0) ||
|
|
836
|
+
!pos.lpShares.eq(ZERO)
|
|
837
|
+
);
|
|
838
|
+
return activePositions.find(
|
|
839
|
+
(position) => position.marketIndex === marketIndex
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private getEmptyPosition(marketIndex: number): PerpPosition {
|
|
844
|
+
return {
|
|
845
|
+
baseAssetAmount: ZERO,
|
|
846
|
+
remainderBaseAssetAmount: 0,
|
|
847
|
+
lastCumulativeFundingRate: ZERO,
|
|
848
|
+
marketIndex,
|
|
849
|
+
quoteAssetAmount: ZERO,
|
|
850
|
+
quoteEntryAmount: ZERO,
|
|
851
|
+
quoteBreakEvenAmount: ZERO,
|
|
852
|
+
openOrders: 0,
|
|
853
|
+
openBids: ZERO,
|
|
854
|
+
openAsks: ZERO,
|
|
855
|
+
settledPnl: ZERO,
|
|
856
|
+
lpShares: ZERO,
|
|
857
|
+
lastBaseAssetAmountPerLp: ZERO,
|
|
858
|
+
lastQuoteAssetAmountPerLp: ZERO,
|
|
859
|
+
perLpBase: 0,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private getClonedPosition(position: PerpPosition): PerpPosition {
|
|
864
|
+
const clonedPosition = Object.assign({}, position);
|
|
865
|
+
return clonedPosition;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
public getMaintenanceMarginRequirement(): BN {
|
|
869
|
+
if (!this.isInitialized) throw new Error("Drift user not initialized");
|
|
870
|
+
|
|
871
|
+
// if user being liq'd, can continue to be liq'd until total collateral above the margin requirement plus buffer
|
|
872
|
+
let liquidationBuffer: BN | undefined = undefined;
|
|
873
|
+
if (this.isBeingLiquidated()) {
|
|
874
|
+
liquidationBuffer = new BN(
|
|
875
|
+
this.driftClient.getStateAccount().liquidationMarginBufferRatio
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return this.getMarginRequirement('Maintenance', liquidationBuffer);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private getMarginRequirement(
|
|
883
|
+
marginCategory: MarginCategory,
|
|
884
|
+
liquidationBuffer?: BN,
|
|
885
|
+
strict = false,
|
|
886
|
+
includeOpenOrders = true
|
|
887
|
+
): BN {
|
|
888
|
+
return this.getTotalPerpPositionLiability(
|
|
889
|
+
marginCategory,
|
|
890
|
+
liquidationBuffer,
|
|
891
|
+
includeOpenOrders,
|
|
892
|
+
strict
|
|
893
|
+
).add(
|
|
894
|
+
this.getSpotMarketLiabilityValue(
|
|
895
|
+
marginCategory,
|
|
896
|
+
undefined,
|
|
897
|
+
liquidationBuffer,
|
|
898
|
+
includeOpenOrders,
|
|
899
|
+
strict
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private getTotalPerpPositionLiability(
|
|
905
|
+
marginCategory?: MarginCategory,
|
|
906
|
+
liquidationBuffer?: BN,
|
|
907
|
+
includeOpenOrders?: boolean,
|
|
908
|
+
strict = false
|
|
909
|
+
): BN {
|
|
910
|
+
return this.getActivePerpPositions().reduce(
|
|
911
|
+
(totalPerpValue, perpPosition) => {
|
|
912
|
+
const baseAssetValue = this.calculateWeightedPerpPositionLiability(
|
|
913
|
+
perpPosition,
|
|
914
|
+
marginCategory,
|
|
915
|
+
liquidationBuffer,
|
|
916
|
+
includeOpenOrders,
|
|
917
|
+
strict
|
|
918
|
+
);
|
|
919
|
+
return totalPerpValue.add(baseAssetValue);
|
|
920
|
+
},
|
|
921
|
+
ZERO
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private calculateWeightedPerpPositionLiability(
|
|
926
|
+
perpPosition: PerpPosition,
|
|
927
|
+
marginCategory?: MarginCategory,
|
|
928
|
+
liquidationBuffer?: BN,
|
|
929
|
+
includeOpenOrders?: boolean,
|
|
930
|
+
strict = false
|
|
931
|
+
): BN {
|
|
932
|
+
const market = this.driftClient.getPerpMarketAccount(
|
|
933
|
+
perpPosition.marketIndex
|
|
934
|
+
)!;
|
|
935
|
+
|
|
936
|
+
if (perpPosition.lpShares.gt(ZERO)) {
|
|
937
|
+
// is an lp, clone so we dont mutate the position
|
|
938
|
+
perpPosition = this.getPerpPositionWithLPSettle(
|
|
939
|
+
market.marketIndex,
|
|
940
|
+
this.getClonedPosition(perpPosition),
|
|
941
|
+
!!marginCategory
|
|
942
|
+
)[0];
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
let valuationPrice = this.driftClient.getOracleDataForPerpMarket(
|
|
946
|
+
market.marketIndex
|
|
947
|
+
).price;
|
|
948
|
+
|
|
949
|
+
if (isVariant(market.status, 'settlement')) {
|
|
950
|
+
valuationPrice = market.expiryPrice;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
let baseAssetAmount: BN;
|
|
954
|
+
let liabilityValue;
|
|
955
|
+
if (includeOpenOrders) {
|
|
956
|
+
const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } =
|
|
957
|
+
calculateWorstCasePerpLiabilityValue(
|
|
958
|
+
perpPosition,
|
|
959
|
+
market,
|
|
960
|
+
valuationPrice
|
|
961
|
+
);
|
|
962
|
+
baseAssetAmount = worstCaseBaseAssetAmount;
|
|
963
|
+
liabilityValue = worstCaseLiabilityValue;
|
|
964
|
+
} else {
|
|
965
|
+
baseAssetAmount = perpPosition.baseAssetAmount;
|
|
966
|
+
liabilityValue = calculatePerpLiabilityValue(
|
|
967
|
+
baseAssetAmount,
|
|
968
|
+
valuationPrice,
|
|
969
|
+
isVariant(market.contractType, 'prediction')
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (marginCategory) {
|
|
974
|
+
let marginRatio = new BN(
|
|
975
|
+
calculateMarketMarginRatio(
|
|
976
|
+
market,
|
|
977
|
+
baseAssetAmount.abs(),
|
|
978
|
+
marginCategory,
|
|
979
|
+
this.userAccount!.maxMarginRatio,
|
|
980
|
+
isVariant(this.userAccount!.marginMode, 'highLeverage')
|
|
981
|
+
)
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
if (liquidationBuffer !== undefined) {
|
|
985
|
+
marginRatio = marginRatio.add(liquidationBuffer);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (isVariant(market.status, 'settlement')) {
|
|
989
|
+
marginRatio = ZERO;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
|
|
993
|
+
market.quoteSpotMarketIndex
|
|
994
|
+
)!;
|
|
995
|
+
const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(
|
|
996
|
+
QUOTE_SPOT_MARKET_INDEX
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
let quotePrice;
|
|
1000
|
+
if (strict) {
|
|
1001
|
+
quotePrice = BN.max(
|
|
1002
|
+
quoteOraclePriceData.price,
|
|
1003
|
+
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
|
|
1004
|
+
);
|
|
1005
|
+
} else {
|
|
1006
|
+
quotePrice = quoteOraclePriceData.price;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
liabilityValue = liabilityValue
|
|
1010
|
+
.mul(quotePrice)
|
|
1011
|
+
.div(PRICE_PRECISION)
|
|
1012
|
+
.mul(marginRatio)
|
|
1013
|
+
.div(MARGIN_PRECISION);
|
|
1014
|
+
|
|
1015
|
+
if (includeOpenOrders) {
|
|
1016
|
+
liabilityValue = liabilityValue.add(
|
|
1017
|
+
new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
if (perpPosition.lpShares.gt(ZERO)) {
|
|
1021
|
+
liabilityValue = liabilityValue.add(
|
|
1022
|
+
BN.max(
|
|
1023
|
+
QUOTE_PRECISION,
|
|
1024
|
+
valuationPrice
|
|
1025
|
+
.mul(market.amm.orderStepSize)
|
|
1026
|
+
.mul(QUOTE_PRECISION)
|
|
1027
|
+
.div(AMM_RESERVE_PRECISION)
|
|
1028
|
+
.div(PRICE_PRECISION)
|
|
1029
|
+
)
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return liabilityValue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private getSpotMarketLiabilityValue(
|
|
1039
|
+
marginCategory: MarginCategory,
|
|
1040
|
+
marketIndex?: number,
|
|
1041
|
+
liquidationBuffer?: BN,
|
|
1042
|
+
includeOpenOrders?: boolean,
|
|
1043
|
+
strict = false,
|
|
1044
|
+
now?: BN
|
|
1045
|
+
): BN {
|
|
1046
|
+
const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(
|
|
1047
|
+
marginCategory,
|
|
1048
|
+
marketIndex,
|
|
1049
|
+
liquidationBuffer,
|
|
1050
|
+
includeOpenOrders,
|
|
1051
|
+
strict,
|
|
1052
|
+
now
|
|
1053
|
+
);
|
|
1054
|
+
return totalLiabilityValue;
|
|
1055
|
+
}
|
|
1056
|
+
}
|