@drift-labs/sdk 2.149.0-beta.0 → 2.149.0-beta.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/VERSION +1 -1
- package/lib/browser/marginCalculation.d.ts +79 -0
- package/lib/browser/marginCalculation.js +196 -0
- package/lib/browser/math/margin.d.ts +1 -1
- package/lib/browser/math/margin.js +8 -2
- package/lib/browser/math/spotPosition.d.ts +1 -1
- package/lib/browser/math/spotPosition.js +3 -2
- package/lib/browser/types.d.ts +10 -0
- package/lib/browser/types.js +7 -1
- package/lib/browser/user.d.ts +56 -16
- package/lib/browser/user.js +414 -46
- package/lib/node/marginCalculation.d.ts +80 -0
- package/lib/node/marginCalculation.d.ts.map +1 -0
- package/lib/node/marginCalculation.js +196 -0
- package/lib/node/math/margin.d.ts +1 -1
- package/lib/node/math/margin.d.ts.map +1 -1
- package/lib/node/math/margin.js +8 -2
- package/lib/node/math/spotPosition.d.ts +1 -1
- package/lib/node/math/spotPosition.d.ts.map +1 -1
- package/lib/node/math/spotPosition.js +3 -2
- package/lib/node/types.d.ts +10 -0
- package/lib/node/types.d.ts.map +1 -1
- package/lib/node/types.js +7 -1
- package/lib/node/user.d.ts +56 -16
- package/lib/node/user.d.ts.map +1 -1
- package/lib/node/user.js +414 -46
- package/package.json +2 -1
- package/src/marginCalculation.ts +287 -0
- package/src/math/margin.ts +15 -2
- package/src/math/spotPosition.ts +6 -2
- package/src/types.ts +12 -0
- package/src/user.ts +737 -87
- package/tests/dlob/helpers.ts +19 -0
- package/tests/user/getMarginCalculation.ts +361 -0
- package/tests/user/helpers.ts +96 -2
- package/tests/user/liquidations.ts +129 -0
package/src/user.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
UserAccount,
|
|
15
15
|
UserStatus,
|
|
16
16
|
UserStatsAccount,
|
|
17
|
+
AccountLiquidatableStatus,
|
|
17
18
|
} from './types';
|
|
18
19
|
import {
|
|
19
20
|
calculateEntryPrice,
|
|
@@ -68,6 +69,7 @@ import { getUser30dRollingVolumeEstimate } from './math/trade';
|
|
|
68
69
|
import {
|
|
69
70
|
MarketType,
|
|
70
71
|
PositionDirection,
|
|
72
|
+
PositionFlag,
|
|
71
73
|
SpotBalanceType,
|
|
72
74
|
SpotMarketAccount,
|
|
73
75
|
} from './types';
|
|
@@ -106,6 +108,13 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice';
|
|
|
106
108
|
|
|
107
109
|
import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel';
|
|
108
110
|
import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber';
|
|
111
|
+
import {
|
|
112
|
+
IsolatedMarginCalculation,
|
|
113
|
+
MarginCalculation,
|
|
114
|
+
MarginContext,
|
|
115
|
+
} from './marginCalculation';
|
|
116
|
+
|
|
117
|
+
export type MarginType = 'Cross' | 'Isolated';
|
|
109
118
|
|
|
110
119
|
export class User {
|
|
111
120
|
driftClient: DriftClient;
|
|
@@ -343,6 +352,23 @@ export class User {
|
|
|
343
352
|
};
|
|
344
353
|
}
|
|
345
354
|
|
|
355
|
+
public getIsolatePerpPositionTokenAmount(perpMarketIndex: number): BN {
|
|
356
|
+
const perpPosition = this.getPerpPosition(perpMarketIndex);
|
|
357
|
+
if (!perpPosition) return ZERO;
|
|
358
|
+
const perpMarket = this.driftClient.getPerpMarketAccount(perpMarketIndex);
|
|
359
|
+
const spotMarket = this.driftClient.getSpotMarketAccount(
|
|
360
|
+
perpMarket.quoteSpotMarketIndex
|
|
361
|
+
);
|
|
362
|
+
if (perpPosition === undefined) {
|
|
363
|
+
return ZERO;
|
|
364
|
+
}
|
|
365
|
+
return getTokenAmount(
|
|
366
|
+
perpPosition.isolatedPositionScaledBalance ?? ZERO, //TODO remove ? later
|
|
367
|
+
spotMarket,
|
|
368
|
+
SpotBalanceType.DEPOSIT
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
346
372
|
public getClonedPosition(position: PerpPosition): PerpPosition {
|
|
347
373
|
const clonedPosition = Object.assign({}, position);
|
|
348
374
|
return clonedPosition;
|
|
@@ -515,62 +541,130 @@ export class User {
|
|
|
515
541
|
*/
|
|
516
542
|
public getFreeCollateral(
|
|
517
543
|
marginCategory: MarginCategory = 'Initial',
|
|
518
|
-
enterHighLeverageMode =
|
|
544
|
+
enterHighLeverageMode = false,
|
|
545
|
+
perpMarketIndex?: number
|
|
519
546
|
): BN {
|
|
520
|
-
const totalCollateral
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
547
|
+
const { totalCollateral, marginRequirement, getIsolatedFreeCollateral } =
|
|
548
|
+
this.getMarginCalculation(marginCategory, {
|
|
549
|
+
enteringHighLeverage: enterHighLeverageMode,
|
|
550
|
+
strict: marginCategory === 'Initial',
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (perpMarketIndex !== undefined) {
|
|
554
|
+
return getIsolatedFreeCollateral(perpMarketIndex);
|
|
555
|
+
} else {
|
|
556
|
+
const freeCollateral = totalCollateral.sub(marginRequirement);
|
|
557
|
+
return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
|
|
558
|
+
}
|
|
527
559
|
}
|
|
528
560
|
|
|
529
561
|
/**
|
|
530
|
-
* @
|
|
562
|
+
* @deprecated Use the overload that includes { marginType, perpMarketIndex }
|
|
531
563
|
*/
|
|
532
564
|
public getMarginRequirement(
|
|
533
565
|
marginCategory: MarginCategory,
|
|
534
566
|
liquidationBuffer?: BN,
|
|
535
|
-
strict
|
|
536
|
-
includeOpenOrders
|
|
537
|
-
enteringHighLeverage
|
|
567
|
+
strict?: boolean,
|
|
568
|
+
includeOpenOrders?: boolean,
|
|
569
|
+
enteringHighLeverage?: boolean
|
|
570
|
+
): BN;
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Calculates the margin requirement based on the specified parameters.
|
|
574
|
+
*
|
|
575
|
+
* @param marginCategory - The category of margin to calculate ('Initial' or 'Maintenance').
|
|
576
|
+
* @param liquidationBuffer - Optional buffer amount to consider during liquidation scenarios.
|
|
577
|
+
* @param strict - Optional flag to enforce strict margin calculations.
|
|
578
|
+
* @param includeOpenOrders - Optional flag to include open orders in the margin calculation.
|
|
579
|
+
* @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode.
|
|
580
|
+
* @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'.
|
|
581
|
+
*
|
|
582
|
+
* @returns The calculated margin requirement as a BN (BigNumber).
|
|
583
|
+
*/
|
|
584
|
+
public getMarginRequirement(
|
|
585
|
+
marginCategory: MarginCategory,
|
|
586
|
+
liquidationBuffer?: BN,
|
|
587
|
+
strict?: boolean,
|
|
588
|
+
includeOpenOrders?: boolean,
|
|
589
|
+
enteringHighLeverage?: boolean,
|
|
590
|
+
perpMarketIndex?: number
|
|
591
|
+
): BN;
|
|
592
|
+
|
|
593
|
+
public getMarginRequirement(
|
|
594
|
+
marginCategory: MarginCategory,
|
|
595
|
+
liquidationBuffer?: BN,
|
|
596
|
+
strict?: boolean,
|
|
597
|
+
includeOpenOrders?: boolean,
|
|
598
|
+
enteringHighLeverage?: boolean,
|
|
599
|
+
perpMarketIndex?: number
|
|
538
600
|
): BN {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
liquidationBuffer
|
|
542
|
-
|
|
601
|
+
const liquidationBufferMap = new Map();
|
|
602
|
+
if (liquidationBuffer && perpMarketIndex !== undefined) {
|
|
603
|
+
liquidationBufferMap.set(perpMarketIndex, liquidationBuffer);
|
|
604
|
+
} else if (liquidationBuffer) {
|
|
605
|
+
liquidationBufferMap.set('cross', liquidationBuffer);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const marginCalc = this.getMarginCalculation(marginCategory, {
|
|
543
609
|
strict,
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
610
|
+
includeOpenOrders,
|
|
611
|
+
enteringHighLeverage,
|
|
612
|
+
liquidationBufferMap,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// If perpMarketIndex is provided, compute only for that market index
|
|
616
|
+
if (perpMarketIndex !== undefined) {
|
|
617
|
+
const isolatedMarginCalculation =
|
|
618
|
+
marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
|
|
619
|
+
if (!isolatedMarginCalculation) return ZERO;
|
|
620
|
+
const { marginRequirement, marginRequirementPlusBuffer } =
|
|
621
|
+
isolatedMarginCalculation;
|
|
622
|
+
|
|
623
|
+
if (liquidationBuffer?.gt(ZERO)) {
|
|
624
|
+
return marginRequirementPlusBuffer;
|
|
625
|
+
}
|
|
626
|
+
return marginRequirement;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Default: Cross margin requirement
|
|
630
|
+
if (liquidationBuffer?.gt(ZERO)) {
|
|
631
|
+
return marginCalc.marginRequirementPlusBuffer;
|
|
632
|
+
}
|
|
633
|
+
return marginCalc.marginRequirement;
|
|
554
634
|
}
|
|
555
635
|
|
|
556
636
|
/**
|
|
557
637
|
* @returns The initial margin requirement in USDC. : QUOTE_PRECISION
|
|
558
638
|
*/
|
|
559
|
-
public getInitialMarginRequirement(
|
|
639
|
+
public getInitialMarginRequirement(
|
|
640
|
+
enterHighLeverageMode = false,
|
|
641
|
+
perpMarketIndex?: number
|
|
642
|
+
): BN {
|
|
560
643
|
return this.getMarginRequirement(
|
|
561
644
|
'Initial',
|
|
562
645
|
undefined,
|
|
563
646
|
true,
|
|
564
647
|
undefined,
|
|
565
|
-
enterHighLeverageMode
|
|
648
|
+
enterHighLeverageMode,
|
|
649
|
+
perpMarketIndex
|
|
566
650
|
);
|
|
567
651
|
}
|
|
568
652
|
|
|
569
653
|
/**
|
|
570
654
|
* @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION
|
|
571
655
|
*/
|
|
572
|
-
public getMaintenanceMarginRequirement(
|
|
573
|
-
|
|
656
|
+
public getMaintenanceMarginRequirement(
|
|
657
|
+
liquidationBuffer?: BN,
|
|
658
|
+
perpMarketIndex?: number
|
|
659
|
+
): BN {
|
|
660
|
+
return this.getMarginRequirement(
|
|
661
|
+
'Maintenance',
|
|
662
|
+
liquidationBuffer,
|
|
663
|
+
false, // strict default
|
|
664
|
+
true, // includeOpenOrders default
|
|
665
|
+
false, // enteringHighLeverage default
|
|
666
|
+
perpMarketIndex
|
|
667
|
+
);
|
|
574
668
|
}
|
|
575
669
|
|
|
576
670
|
public getActivePerpPositionsForUserAccount(
|
|
@@ -580,7 +674,8 @@ export class User {
|
|
|
580
674
|
(pos) =>
|
|
581
675
|
!pos.baseAssetAmount.eq(ZERO) ||
|
|
582
676
|
!pos.quoteAssetAmount.eq(ZERO) ||
|
|
583
|
-
!(pos.openOrders == 0)
|
|
677
|
+
!(pos.openOrders == 0) ||
|
|
678
|
+
pos.isolatedPositionScaledBalance?.gt(ZERO)
|
|
584
679
|
);
|
|
585
680
|
}
|
|
586
681
|
|
|
@@ -1153,46 +1248,93 @@ export class User {
|
|
|
1153
1248
|
marginCategory: MarginCategory = 'Initial',
|
|
1154
1249
|
strict = false,
|
|
1155
1250
|
includeOpenOrders = true,
|
|
1156
|
-
liquidationBuffer?: BN
|
|
1251
|
+
liquidationBuffer?: BN,
|
|
1252
|
+
perpMarketIndex?: number
|
|
1157
1253
|
): BN {
|
|
1158
|
-
|
|
1159
|
-
undefined
|
|
1160
|
-
|
|
1254
|
+
const liquidationBufferMap = (() => {
|
|
1255
|
+
if (liquidationBuffer && perpMarketIndex !== undefined) {
|
|
1256
|
+
return new Map([[perpMarketIndex, liquidationBuffer]]);
|
|
1257
|
+
} else if (liquidationBuffer) {
|
|
1258
|
+
return new Map([['cross', liquidationBuffer]]);
|
|
1259
|
+
}
|
|
1260
|
+
return new Map();
|
|
1261
|
+
})();
|
|
1262
|
+
const marginCalc = this.getMarginCalculation(marginCategory, {
|
|
1263
|
+
strict,
|
|
1161
1264
|
includeOpenOrders,
|
|
1162
|
-
|
|
1163
|
-
)
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1265
|
+
liquidationBufferMap,
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
if (perpMarketIndex !== undefined) {
|
|
1269
|
+
const { totalCollateral, totalCollateralBuffer } =
|
|
1270
|
+
marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
|
|
1271
|
+
if (liquidationBuffer?.gt(ZERO)) {
|
|
1272
|
+
return totalCollateralBuffer;
|
|
1273
|
+
}
|
|
1274
|
+
return totalCollateral;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (liquidationBuffer?.gt(ZERO)) {
|
|
1278
|
+
return marginCalc.totalCollateralBuffer;
|
|
1279
|
+
}
|
|
1280
|
+
return marginCalc.totalCollateral;
|
|
1172
1281
|
}
|
|
1173
1282
|
|
|
1174
|
-
public getLiquidationBuffer():
|
|
1175
|
-
|
|
1176
|
-
let liquidationBuffer = undefined;
|
|
1283
|
+
public getLiquidationBuffer(): Map<number | 'cross', BN> {
|
|
1284
|
+
const liquidationBufferMap = new Map<number | 'cross', BN>();
|
|
1177
1285
|
if (this.isBeingLiquidated()) {
|
|
1178
|
-
|
|
1179
|
-
|
|
1286
|
+
liquidationBufferMap.set(
|
|
1287
|
+
'cross',
|
|
1288
|
+
new BN(this.driftClient.getStateAccount().liquidationMarginBufferRatio)
|
|
1180
1289
|
);
|
|
1181
1290
|
}
|
|
1182
|
-
|
|
1291
|
+
for (const position of this.getActivePerpPositions()) {
|
|
1292
|
+
if (
|
|
1293
|
+
position.positionFlag &
|
|
1294
|
+
(PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)
|
|
1295
|
+
) {
|
|
1296
|
+
liquidationBufferMap.set(
|
|
1297
|
+
position.marketIndex,
|
|
1298
|
+
new BN(
|
|
1299
|
+
this.driftClient.getStateAccount().liquidationMarginBufferRatio
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return liquidationBufferMap;
|
|
1183
1305
|
}
|
|
1184
1306
|
|
|
1185
1307
|
/**
|
|
1186
1308
|
* calculates User Health by comparing total collateral and maint. margin requirement
|
|
1187
1309
|
* @returns : number (value from [0, 100])
|
|
1188
1310
|
*/
|
|
1189
|
-
public getHealth(): number {
|
|
1190
|
-
if (this.
|
|
1311
|
+
public getHealth(perpMarketIndex?: number): number {
|
|
1312
|
+
if (this.isCrossMarginBeingLiquidated() && !perpMarketIndex) {
|
|
1313
|
+
return 0;
|
|
1314
|
+
}
|
|
1315
|
+
if (
|
|
1316
|
+
perpMarketIndex &&
|
|
1317
|
+
this.isIsolatedPositionBeingLiquidated(perpMarketIndex)
|
|
1318
|
+
) {
|
|
1191
1319
|
return 0;
|
|
1192
1320
|
}
|
|
1193
1321
|
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1322
|
+
const marginCalc = this.getMarginCalculation('Maintenance');
|
|
1323
|
+
|
|
1324
|
+
let totalCollateral: BN;
|
|
1325
|
+
let maintenanceMarginReq: BN;
|
|
1326
|
+
|
|
1327
|
+
if (perpMarketIndex) {
|
|
1328
|
+
const isolatedMarginCalc =
|
|
1329
|
+
marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
|
|
1330
|
+
if (isolatedMarginCalc) {
|
|
1331
|
+
totalCollateral = isolatedMarginCalc.totalCollateral;
|
|
1332
|
+
maintenanceMarginReq = isolatedMarginCalc.marginRequirement;
|
|
1333
|
+
}
|
|
1334
|
+
} else {
|
|
1335
|
+
totalCollateral = marginCalc.totalCollateral;
|
|
1336
|
+
maintenanceMarginReq = marginCalc.marginRequirement;
|
|
1337
|
+
}
|
|
1196
1338
|
|
|
1197
1339
|
let health: number;
|
|
1198
1340
|
|
|
@@ -1491,9 +1633,9 @@ export class User {
|
|
|
1491
1633
|
* calculates current user leverage which is (total liability size) / (net asset value)
|
|
1492
1634
|
* @returns : Precision TEN_THOUSAND
|
|
1493
1635
|
*/
|
|
1494
|
-
public getLeverage(includeOpenOrders = true): BN {
|
|
1636
|
+
public getLeverage(includeOpenOrders = true, perpMarketIndex?: number): BN {
|
|
1495
1637
|
return this.calculateLeverageFromComponents(
|
|
1496
|
-
this.getLeverageComponents(includeOpenOrders)
|
|
1638
|
+
this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex)
|
|
1497
1639
|
);
|
|
1498
1640
|
}
|
|
1499
1641
|
|
|
@@ -1521,13 +1663,67 @@ export class User {
|
|
|
1521
1663
|
|
|
1522
1664
|
getLeverageComponents(
|
|
1523
1665
|
includeOpenOrders = true,
|
|
1524
|
-
marginCategory: MarginCategory = undefined
|
|
1666
|
+
marginCategory: MarginCategory = undefined,
|
|
1667
|
+
perpMarketIndex?: number
|
|
1525
1668
|
): {
|
|
1526
1669
|
perpLiabilityValue: BN;
|
|
1527
1670
|
perpPnl: BN;
|
|
1528
1671
|
spotAssetValue: BN;
|
|
1529
1672
|
spotLiabilityValue: BN;
|
|
1530
1673
|
} {
|
|
1674
|
+
if (perpMarketIndex) {
|
|
1675
|
+
const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex);
|
|
1676
|
+
const perpLiability = this.calculateWeightedPerpPositionLiability(
|
|
1677
|
+
perpPosition,
|
|
1678
|
+
marginCategory,
|
|
1679
|
+
undefined,
|
|
1680
|
+
includeOpenOrders
|
|
1681
|
+
);
|
|
1682
|
+
const perpMarket = this.driftClient.getPerpMarketAccount(
|
|
1683
|
+
perpPosition.marketIndex
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
const oraclePriceData = this.getOracleDataForPerpMarket(
|
|
1687
|
+
perpPosition.marketIndex
|
|
1688
|
+
);
|
|
1689
|
+
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
|
|
1690
|
+
perpMarket.quoteSpotMarketIndex
|
|
1691
|
+
);
|
|
1692
|
+
const quoteOraclePriceData = this.getOracleDataForSpotMarket(
|
|
1693
|
+
perpMarket.quoteSpotMarketIndex
|
|
1694
|
+
);
|
|
1695
|
+
const strictOracle = new StrictOraclePrice(
|
|
1696
|
+
quoteOraclePriceData.price,
|
|
1697
|
+
quoteOraclePriceData.twap
|
|
1698
|
+
);
|
|
1699
|
+
|
|
1700
|
+
const positionUnrealizedPnl = calculatePositionPNL(
|
|
1701
|
+
perpMarket,
|
|
1702
|
+
perpPosition,
|
|
1703
|
+
true,
|
|
1704
|
+
oraclePriceData
|
|
1705
|
+
);
|
|
1706
|
+
|
|
1707
|
+
const tokenAmount = getTokenAmount(
|
|
1708
|
+
perpPosition.isolatedPositionScaledBalance ?? ZERO,
|
|
1709
|
+
quoteSpotMarket,
|
|
1710
|
+
SpotBalanceType.DEPOSIT
|
|
1711
|
+
);
|
|
1712
|
+
|
|
1713
|
+
const spotAssetValue = getStrictTokenValue(
|
|
1714
|
+
tokenAmount,
|
|
1715
|
+
quoteSpotMarket.decimals,
|
|
1716
|
+
strictOracle
|
|
1717
|
+
);
|
|
1718
|
+
|
|
1719
|
+
return {
|
|
1720
|
+
perpLiabilityValue: perpLiability,
|
|
1721
|
+
perpPnl: positionUnrealizedPnl,
|
|
1722
|
+
spotAssetValue,
|
|
1723
|
+
spotLiabilityValue: ZERO,
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1531
1727
|
const perpLiability = this.getTotalPerpPositionLiability(
|
|
1532
1728
|
marginCategory,
|
|
1533
1729
|
undefined,
|
|
@@ -1817,32 +2013,83 @@ export class User {
|
|
|
1817
2013
|
return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue);
|
|
1818
2014
|
}
|
|
1819
2015
|
|
|
1820
|
-
public canBeLiquidated(): {
|
|
1821
|
-
|
|
1822
|
-
marginRequirement: BN;
|
|
1823
|
-
totalCollateral: BN;
|
|
2016
|
+
public canBeLiquidated(): AccountLiquidatableStatus & {
|
|
2017
|
+
isolatedPositions: Map<number, AccountLiquidatableStatus>;
|
|
1824
2018
|
} {
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2019
|
+
// Deprecated signature retained for backward compatibility in type only
|
|
2020
|
+
// but implementation now delegates to the new Map-based API and returns cross margin status.
|
|
2021
|
+
const map = this.getLiquidationStatuses();
|
|
2022
|
+
const cross = map.get('cross');
|
|
2023
|
+
const isolatedPositions: Map<number, AccountLiquidatableStatus> = new Map(
|
|
2024
|
+
Array.from(map.entries())
|
|
2025
|
+
.filter(
|
|
2026
|
+
(e): e is [number, AccountLiquidatableStatus] => e[0] !== 'cross'
|
|
2027
|
+
)
|
|
2028
|
+
.map(([key, value]) => [key, value])
|
|
1832
2029
|
);
|
|
2030
|
+
return cross
|
|
2031
|
+
? { ...cross, isolatedPositions }
|
|
2032
|
+
: {
|
|
2033
|
+
canBeLiquidated: false,
|
|
2034
|
+
marginRequirement: ZERO,
|
|
2035
|
+
totalCollateral: ZERO,
|
|
2036
|
+
isolatedPositions,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
1833
2039
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2040
|
+
/**
|
|
2041
|
+
* New API: Returns liquidation status for cross and each isolated perp position.
|
|
2042
|
+
* Map keys:
|
|
2043
|
+
* - 'cross' for cross margin
|
|
2044
|
+
* - marketIndex (number) for each isolated perp position
|
|
2045
|
+
*/
|
|
2046
|
+
public getLiquidationStatuses(
|
|
2047
|
+
marginCalc?: MarginCalculation
|
|
2048
|
+
): Map<'cross' | number, AccountLiquidatableStatus> {
|
|
2049
|
+
// If not provided, use buffer-aware calc for canBeLiquidated checks
|
|
2050
|
+
if (!marginCalc) {
|
|
2051
|
+
const liquidationBufferMap = this.getLiquidationBuffer();
|
|
2052
|
+
marginCalc = this.getMarginCalculation('Maintenance', {
|
|
2053
|
+
liquidationBufferMap,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
1837
2056
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2057
|
+
const result = new Map<'cross' | number, AccountLiquidatableStatus>();
|
|
2058
|
+
|
|
2059
|
+
// Cross margin status
|
|
2060
|
+
const crossTotalCollateral = marginCalc.totalCollateral;
|
|
2061
|
+
const crossMarginRequirement = marginCalc.marginRequirement;
|
|
2062
|
+
result.set('cross', {
|
|
2063
|
+
canBeLiquidated: crossTotalCollateral.lt(crossMarginRequirement),
|
|
2064
|
+
marginRequirement: crossMarginRequirement,
|
|
2065
|
+
totalCollateral: crossTotalCollateral,
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
// Isolated positions status
|
|
2069
|
+
for (const [
|
|
2070
|
+
marketIndex,
|
|
2071
|
+
isoCalc,
|
|
2072
|
+
] of marginCalc.isolatedMarginCalculations) {
|
|
2073
|
+
const isoTotalCollateral = isoCalc.totalCollateral;
|
|
2074
|
+
const isoMarginRequirement = isoCalc.marginRequirement;
|
|
2075
|
+
result.set(marketIndex, {
|
|
2076
|
+
canBeLiquidated: isoTotalCollateral.lt(isoMarginRequirement),
|
|
2077
|
+
marginRequirement: isoMarginRequirement,
|
|
2078
|
+
totalCollateral: isoTotalCollateral,
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
return result;
|
|
1843
2083
|
}
|
|
1844
2084
|
|
|
1845
2085
|
public isBeingLiquidated(): boolean {
|
|
2086
|
+
return (
|
|
2087
|
+
this.isCrossMarginBeingLiquidated() ||
|
|
2088
|
+
this.hasIsolatedPositionBeingLiquidated()
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
public isCrossMarginBeingLiquidated(): boolean {
|
|
1846
2093
|
return (
|
|
1847
2094
|
(this.getUserAccount().status &
|
|
1848
2095
|
(UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) >
|
|
@@ -1850,6 +2097,55 @@ export class User {
|
|
|
1850
2097
|
);
|
|
1851
2098
|
}
|
|
1852
2099
|
|
|
2100
|
+
/** Returns true if cross margin is currently below maintenance requirement (no buffer). */
|
|
2101
|
+
public canCrossMarginBeLiquidated(marginCalc?: MarginCalculation): boolean {
|
|
2102
|
+
const calc = marginCalc ?? this.getMarginCalculation('Maintenance');
|
|
2103
|
+
return calc.totalCollateral.lt(calc.marginRequirement);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
public hasIsolatedPositionBeingLiquidated(): boolean {
|
|
2107
|
+
return this.getActivePerpPositions().some(
|
|
2108
|
+
(position) =>
|
|
2109
|
+
(position.positionFlag &
|
|
2110
|
+
(PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) >
|
|
2111
|
+
0
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
public isIsolatedPositionBeingLiquidated(perpMarketIndex: number): boolean {
|
|
2116
|
+
const position = this.getActivePerpPositions().find(
|
|
2117
|
+
(position) => position.marketIndex === perpMarketIndex
|
|
2118
|
+
);
|
|
2119
|
+
|
|
2120
|
+
return (
|
|
2121
|
+
(position?.positionFlag &
|
|
2122
|
+
(PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) >
|
|
2123
|
+
0
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */
|
|
2128
|
+
public getLiquidatableIsolatedPositions(
|
|
2129
|
+
marginCalc?: MarginCalculation
|
|
2130
|
+
): number[] {
|
|
2131
|
+
const liquidatableIsolatedPositions = [];
|
|
2132
|
+
const calc = marginCalc ?? this.getMarginCalculation('Maintenance');
|
|
2133
|
+
for (const [marketIndex, isoCalc] of calc.isolatedMarginCalculations) {
|
|
2134
|
+
if (this.canIsolatedPositionMarginBeLiquidated(isoCalc)) {
|
|
2135
|
+
liquidatableIsolatedPositions.push(marketIndex);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
return liquidatableIsolatedPositions;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
public canIsolatedPositionMarginBeLiquidated(
|
|
2142
|
+
isolatedMarginCalculation: IsolatedMarginCalculation
|
|
2143
|
+
): boolean {
|
|
2144
|
+
return isolatedMarginCalculation.totalCollateral.lt(
|
|
2145
|
+
isolatedMarginCalculation.marginRequirement
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
1853
2149
|
public hasStatus(status: UserStatus): boolean {
|
|
1854
2150
|
return (this.getUserAccount().status & status) > 0;
|
|
1855
2151
|
}
|
|
@@ -2006,13 +2302,68 @@ export class User {
|
|
|
2006
2302
|
marginCategory: MarginCategory = 'Maintenance',
|
|
2007
2303
|
includeOpenOrders = false,
|
|
2008
2304
|
offsetCollateral = ZERO,
|
|
2009
|
-
enteringHighLeverage =
|
|
2305
|
+
enteringHighLeverage = false,
|
|
2306
|
+
marginType?: MarginType
|
|
2010
2307
|
): BN {
|
|
2308
|
+
const market = this.driftClient.getPerpMarketAccount(marketIndex);
|
|
2309
|
+
|
|
2310
|
+
const oracle =
|
|
2311
|
+
this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle;
|
|
2312
|
+
|
|
2313
|
+
const oraclePrice =
|
|
2314
|
+
this.driftClient.getOracleDataForPerpMarket(marketIndex).price;
|
|
2315
|
+
|
|
2316
|
+
const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex);
|
|
2317
|
+
|
|
2318
|
+
if (marginType === 'Isolated') {
|
|
2319
|
+
const marginCalculation = this.getMarginCalculation(marginCategory, {
|
|
2320
|
+
strict: false,
|
|
2321
|
+
includeOpenOrders,
|
|
2322
|
+
enteringHighLeverage,
|
|
2323
|
+
});
|
|
2324
|
+
const isolatedMarginCalculation =
|
|
2325
|
+
marginCalculation.isolatedMarginCalculations.get(marketIndex);
|
|
2326
|
+
if (!isolatedMarginCalculation) return new BN(-1);
|
|
2327
|
+
const { totalCollateral, marginRequirement } = isolatedMarginCalculation;
|
|
2328
|
+
|
|
2329
|
+
const freeCollateral = BN.max(
|
|
2330
|
+
ZERO,
|
|
2331
|
+
totalCollateral.sub(marginRequirement)
|
|
2332
|
+
).add(offsetCollateral);
|
|
2333
|
+
|
|
2334
|
+
const freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp(
|
|
2335
|
+
market,
|
|
2336
|
+
currentPerpPosition,
|
|
2337
|
+
positionBaseSizeChange,
|
|
2338
|
+
oraclePrice,
|
|
2339
|
+
marginCategory,
|
|
2340
|
+
includeOpenOrders,
|
|
2341
|
+
enteringHighLeverage
|
|
2342
|
+
);
|
|
2343
|
+
|
|
2344
|
+
if (freeCollateralDelta.eq(ZERO)) {
|
|
2345
|
+
return new BN(-1);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
const liqPriceDelta = freeCollateral
|
|
2349
|
+
.mul(QUOTE_PRECISION)
|
|
2350
|
+
.div(freeCollateralDelta);
|
|
2351
|
+
|
|
2352
|
+
const liqPrice = oraclePrice.sub(liqPriceDelta);
|
|
2353
|
+
|
|
2354
|
+
if (liqPrice.lt(ZERO)) {
|
|
2355
|
+
return new BN(-1);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
return liqPrice;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2011
2361
|
const totalCollateral = this.getTotalCollateral(
|
|
2012
2362
|
marginCategory,
|
|
2013
2363
|
false,
|
|
2014
2364
|
includeOpenOrders
|
|
2015
2365
|
);
|
|
2366
|
+
|
|
2016
2367
|
const marginRequirement = this.getMarginRequirement(
|
|
2017
2368
|
marginCategory,
|
|
2018
2369
|
undefined,
|
|
@@ -2020,20 +2371,12 @@ export class User {
|
|
|
2020
2371
|
includeOpenOrders,
|
|
2021
2372
|
enteringHighLeverage
|
|
2022
2373
|
);
|
|
2374
|
+
|
|
2023
2375
|
let freeCollateral = BN.max(
|
|
2024
2376
|
ZERO,
|
|
2025
2377
|
totalCollateral.sub(marginRequirement)
|
|
2026
2378
|
).add(offsetCollateral);
|
|
2027
2379
|
|
|
2028
|
-
const oracle =
|
|
2029
|
-
this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle;
|
|
2030
|
-
|
|
2031
|
-
const oraclePrice =
|
|
2032
|
-
this.driftClient.getOracleDataForPerpMarket(marketIndex).price;
|
|
2033
|
-
|
|
2034
|
-
const market = this.driftClient.getPerpMarketAccount(marketIndex);
|
|
2035
|
-
const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex);
|
|
2036
|
-
|
|
2037
2380
|
positionBaseSizeChange = standardizeBaseAssetAmount(
|
|
2038
2381
|
positionBaseSizeChange,
|
|
2039
2382
|
market.amm.orderStepSize
|
|
@@ -3915,4 +4258,311 @@ export class User {
|
|
|
3915
4258
|
activeSpotPositions: activeSpotMarkets,
|
|
3916
4259
|
};
|
|
3917
4260
|
}
|
|
4261
|
+
|
|
4262
|
+
/**
|
|
4263
|
+
* Compute a consolidated margin snapshot once, without caching.
|
|
4264
|
+
* Consumers can use this to avoid duplicating work across separate calls.
|
|
4265
|
+
*/
|
|
4266
|
+
public getMarginCalculation(
|
|
4267
|
+
marginCategory: MarginCategory = 'Initial',
|
|
4268
|
+
opts?: {
|
|
4269
|
+
strict?: boolean; // mirror StrictOraclePrice application
|
|
4270
|
+
includeOpenOrders?: boolean;
|
|
4271
|
+
enteringHighLeverage?: boolean;
|
|
4272
|
+
liquidationBufferMap?: Map<number | 'cross', BN>; // margin_buffer analog for buffer mode
|
|
4273
|
+
}
|
|
4274
|
+
): MarginCalculation {
|
|
4275
|
+
const strict = opts?.strict ?? false;
|
|
4276
|
+
const enteringHighLeverage = opts?.enteringHighLeverage ?? false;
|
|
4277
|
+
const liquidationBufferMap = opts?.liquidationBufferMap ?? new Map();
|
|
4278
|
+
const includeOpenOrders = opts?.includeOpenOrders ?? true;
|
|
4279
|
+
|
|
4280
|
+
// Equivalent to on-chain user_custom_margin_ratio
|
|
4281
|
+
const userCustomMarginRatio =
|
|
4282
|
+
marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0;
|
|
4283
|
+
|
|
4284
|
+
// Initialize calc via JS mirror of Rust/on-chain MarginCalculation
|
|
4285
|
+
const isolatedMarginBuffers = new Map<number, BN>();
|
|
4286
|
+
for (const [
|
|
4287
|
+
marketIndex,
|
|
4288
|
+
isolatedMarginBuffer,
|
|
4289
|
+
] of opts?.liquidationBufferMap ?? new Map()) {
|
|
4290
|
+
if (marketIndex !== 'cross') {
|
|
4291
|
+
isolatedMarginBuffers.set(marketIndex, isolatedMarginBuffer);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
const ctx = MarginContext.standard(marginCategory)
|
|
4295
|
+
.strictMode(strict)
|
|
4296
|
+
.setCrossMarginBuffer(opts?.liquidationBufferMap?.get('cross') ?? ZERO)
|
|
4297
|
+
.setIsolatedMarginBuffers(isolatedMarginBuffers);
|
|
4298
|
+
const calc = new MarginCalculation(ctx);
|
|
4299
|
+
|
|
4300
|
+
// SPOT POSITIONS
|
|
4301
|
+
for (const spotPosition of this.getUserAccount().spotPositions) {
|
|
4302
|
+
if (isSpotPositionAvailable(spotPosition)) continue;
|
|
4303
|
+
|
|
4304
|
+
const isQuote = spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX;
|
|
4305
|
+
|
|
4306
|
+
const spotMarket = this.driftClient.getSpotMarketAccount(
|
|
4307
|
+
spotPosition.marketIndex
|
|
4308
|
+
);
|
|
4309
|
+
const oraclePriceData = this.getOracleDataForSpotMarket(
|
|
4310
|
+
spotPosition.marketIndex
|
|
4311
|
+
);
|
|
4312
|
+
const twap5 = strict
|
|
4313
|
+
? calculateLiveOracleTwap(
|
|
4314
|
+
spotMarket.historicalOracleData,
|
|
4315
|
+
oraclePriceData,
|
|
4316
|
+
new BN(Math.floor(Date.now() / 1000)),
|
|
4317
|
+
FIVE_MINUTE
|
|
4318
|
+
)
|
|
4319
|
+
: undefined;
|
|
4320
|
+
const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5);
|
|
4321
|
+
|
|
4322
|
+
if (isQuote) {
|
|
4323
|
+
const tokenAmount = getSignedTokenAmount(
|
|
4324
|
+
getTokenAmount(
|
|
4325
|
+
spotPosition.scaledBalance,
|
|
4326
|
+
spotMarket,
|
|
4327
|
+
spotPosition.balanceType
|
|
4328
|
+
),
|
|
4329
|
+
spotPosition.balanceType
|
|
4330
|
+
);
|
|
4331
|
+
if (isVariant(spotPosition.balanceType, 'deposit')) {
|
|
4332
|
+
// add deposit value to total collateral
|
|
4333
|
+
const weightedTokenValue = this.getSpotAssetValue(
|
|
4334
|
+
tokenAmount,
|
|
4335
|
+
strictOracle,
|
|
4336
|
+
spotMarket,
|
|
4337
|
+
marginCategory
|
|
4338
|
+
);
|
|
4339
|
+
calc.addCrossMarginTotalCollateral(weightedTokenValue);
|
|
4340
|
+
} else {
|
|
4341
|
+
// borrow on quote contributes to margin requirement
|
|
4342
|
+
const tokenValueAbs = this.getSpotLiabilityValue(
|
|
4343
|
+
tokenAmount,
|
|
4344
|
+
strictOracle,
|
|
4345
|
+
spotMarket,
|
|
4346
|
+
marginCategory,
|
|
4347
|
+
liquidationBufferMap.get('cross') ?? new BN(0)
|
|
4348
|
+
).abs();
|
|
4349
|
+
calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs);
|
|
4350
|
+
}
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
// Non-quote spot: worst-case simulation
|
|
4355
|
+
const {
|
|
4356
|
+
tokenAmount: worstCaseTokenAmount,
|
|
4357
|
+
ordersValue: worstCaseOrdersValue,
|
|
4358
|
+
} = getWorstCaseTokenAmounts(
|
|
4359
|
+
spotPosition,
|
|
4360
|
+
spotMarket,
|
|
4361
|
+
strictOracle,
|
|
4362
|
+
marginCategory,
|
|
4363
|
+
userCustomMarginRatio,
|
|
4364
|
+
includeOpenOrders
|
|
4365
|
+
// false
|
|
4366
|
+
);
|
|
4367
|
+
|
|
4368
|
+
if (includeOpenOrders) {
|
|
4369
|
+
// open order IM
|
|
4370
|
+
calc.addCrossMarginRequirement(
|
|
4371
|
+
new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT),
|
|
4372
|
+
ZERO
|
|
4373
|
+
);
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
if (worstCaseTokenAmount.gt(ZERO)) {
|
|
4377
|
+
const baseAssetValue = this.getSpotAssetValue(
|
|
4378
|
+
worstCaseTokenAmount,
|
|
4379
|
+
strictOracle,
|
|
4380
|
+
spotMarket,
|
|
4381
|
+
marginCategory
|
|
4382
|
+
);
|
|
4383
|
+
// asset side increases total collateral (weighted)
|
|
4384
|
+
calc.addCrossMarginTotalCollateral(baseAssetValue);
|
|
4385
|
+
} else if (worstCaseTokenAmount.lt(ZERO)) {
|
|
4386
|
+
// liability side increases margin requirement (weighted >= abs(token_value))
|
|
4387
|
+
const getSpotLiabilityValue = this.getSpotLiabilityValue(
|
|
4388
|
+
worstCaseTokenAmount,
|
|
4389
|
+
strictOracle,
|
|
4390
|
+
spotMarket,
|
|
4391
|
+
marginCategory,
|
|
4392
|
+
liquidationBufferMap.get('cross')
|
|
4393
|
+
);
|
|
4394
|
+
|
|
4395
|
+
calc.addCrossMarginRequirement(
|
|
4396
|
+
getSpotLiabilityValue.abs(),
|
|
4397
|
+
getSpotLiabilityValue.abs()
|
|
4398
|
+
);
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
// orders value contributes to collateral or requirement
|
|
4402
|
+
if (worstCaseOrdersValue.gt(ZERO)) {
|
|
4403
|
+
calc.addCrossMarginTotalCollateral(worstCaseOrdersValue);
|
|
4404
|
+
} else if (worstCaseOrdersValue.lt(ZERO)) {
|
|
4405
|
+
const absVal = worstCaseOrdersValue.abs();
|
|
4406
|
+
calc.addCrossMarginRequirement(absVal, absVal);
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
// PERP POSITIONS
|
|
4411
|
+
for (const marketPosition of this.getActivePerpPositions()) {
|
|
4412
|
+
const market = this.driftClient.getPerpMarketAccount(
|
|
4413
|
+
marketPosition.marketIndex
|
|
4414
|
+
);
|
|
4415
|
+
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
|
|
4416
|
+
market.quoteSpotMarketIndex
|
|
4417
|
+
);
|
|
4418
|
+
const quoteOraclePriceData = this.getOracleDataForSpotMarket(
|
|
4419
|
+
market.quoteSpotMarketIndex
|
|
4420
|
+
);
|
|
4421
|
+
const oraclePriceData = this.getMMOracleDataForPerpMarket(
|
|
4422
|
+
market.marketIndex
|
|
4423
|
+
);
|
|
4424
|
+
|
|
4425
|
+
const nonMmmOraclePriceData = this.getOracleDataForPerpMarket(
|
|
4426
|
+
market.marketIndex
|
|
4427
|
+
);
|
|
4428
|
+
|
|
4429
|
+
// Worst-case perp liability and weighted pnl
|
|
4430
|
+
const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } =
|
|
4431
|
+
calculateWorstCasePerpLiabilityValue(
|
|
4432
|
+
marketPosition,
|
|
4433
|
+
market,
|
|
4434
|
+
nonMmmOraclePriceData.price,
|
|
4435
|
+
includeOpenOrders
|
|
4436
|
+
);
|
|
4437
|
+
|
|
4438
|
+
// margin ratio for this perp
|
|
4439
|
+
const customMarginRatio = Math.max(
|
|
4440
|
+
userCustomMarginRatio,
|
|
4441
|
+
marketPosition.maxMarginRatio
|
|
4442
|
+
);
|
|
4443
|
+
let marginRatio = new BN(
|
|
4444
|
+
calculateMarketMarginRatio(
|
|
4445
|
+
market,
|
|
4446
|
+
worstCaseBaseAssetAmount.abs(),
|
|
4447
|
+
marginCategory,
|
|
4448
|
+
customMarginRatio,
|
|
4449
|
+
this.isHighLeverageMode(marginCategory) || enteringHighLeverage
|
|
4450
|
+
)
|
|
4451
|
+
);
|
|
4452
|
+
if (isVariant(market.status, 'settlement')) {
|
|
4453
|
+
marginRatio = ZERO;
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
// convert liability to quote value and apply margin ratio
|
|
4457
|
+
const quotePrice = strict
|
|
4458
|
+
? BN.max(
|
|
4459
|
+
quoteOraclePriceData.price,
|
|
4460
|
+
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
|
|
4461
|
+
)
|
|
4462
|
+
: quoteOraclePriceData.price;
|
|
4463
|
+
let perpMarginRequirement = worstCaseLiabilityValue
|
|
4464
|
+
.mul(quotePrice)
|
|
4465
|
+
.div(PRICE_PRECISION)
|
|
4466
|
+
.mul(marginRatio)
|
|
4467
|
+
.div(MARGIN_PRECISION);
|
|
4468
|
+
// add open orders IM
|
|
4469
|
+
if (includeOpenOrders) {
|
|
4470
|
+
perpMarginRequirement = perpMarginRequirement.add(
|
|
4471
|
+
new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
// weighted unrealized pnl
|
|
4476
|
+
let positionUnrealizedPnl = calculatePositionPNL(
|
|
4477
|
+
market,
|
|
4478
|
+
marketPosition,
|
|
4479
|
+
true,
|
|
4480
|
+
oraclePriceData
|
|
4481
|
+
);
|
|
4482
|
+
let pnlQuotePrice: BN;
|
|
4483
|
+
if (strict && positionUnrealizedPnl.gt(ZERO)) {
|
|
4484
|
+
pnlQuotePrice = BN.min(
|
|
4485
|
+
quoteOraclePriceData.price,
|
|
4486
|
+
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
|
|
4487
|
+
);
|
|
4488
|
+
} else if (strict && positionUnrealizedPnl.lt(ZERO)) {
|
|
4489
|
+
pnlQuotePrice = BN.max(
|
|
4490
|
+
quoteOraclePriceData.price,
|
|
4491
|
+
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
|
|
4492
|
+
);
|
|
4493
|
+
} else {
|
|
4494
|
+
pnlQuotePrice = quoteOraclePriceData.price;
|
|
4495
|
+
}
|
|
4496
|
+
positionUnrealizedPnl = positionUnrealizedPnl
|
|
4497
|
+
.mul(pnlQuotePrice)
|
|
4498
|
+
.div(PRICE_PRECISION);
|
|
4499
|
+
|
|
4500
|
+
if (marginCategory !== undefined) {
|
|
4501
|
+
if (positionUnrealizedPnl.gt(ZERO)) {
|
|
4502
|
+
positionUnrealizedPnl = positionUnrealizedPnl
|
|
4503
|
+
.mul(
|
|
4504
|
+
calculateUnrealizedAssetWeight(
|
|
4505
|
+
market,
|
|
4506
|
+
quoteSpotMarket,
|
|
4507
|
+
positionUnrealizedPnl,
|
|
4508
|
+
marginCategory,
|
|
4509
|
+
oraclePriceData
|
|
4510
|
+
)
|
|
4511
|
+
)
|
|
4512
|
+
.div(new BN(SPOT_MARKET_WEIGHT_PRECISION));
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4516
|
+
// Add perp contribution: isolated vs cross
|
|
4517
|
+
const isIsolated = this.isPerpPositionIsolated(marketPosition);
|
|
4518
|
+
if (isIsolated) {
|
|
4519
|
+
// derive isolated quote deposit value, mirroring on-chain logic
|
|
4520
|
+
let depositValue = ZERO;
|
|
4521
|
+
if (marketPosition.isolatedPositionScaledBalance?.gt(ZERO)) {
|
|
4522
|
+
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
|
|
4523
|
+
market.quoteSpotMarketIndex
|
|
4524
|
+
);
|
|
4525
|
+
const quoteOraclePriceData = this.getOracleDataForSpotMarket(
|
|
4526
|
+
market.quoteSpotMarketIndex
|
|
4527
|
+
);
|
|
4528
|
+
const strictQuote = new StrictOraclePrice(
|
|
4529
|
+
quoteOraclePriceData.price,
|
|
4530
|
+
strict
|
|
4531
|
+
? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
|
|
4532
|
+
: undefined
|
|
4533
|
+
);
|
|
4534
|
+
const quoteTokenAmount = getTokenAmount(
|
|
4535
|
+
marketPosition.isolatedPositionScaledBalance ?? ZERO,
|
|
4536
|
+
quoteSpotMarket,
|
|
4537
|
+
SpotBalanceType.DEPOSIT
|
|
4538
|
+
);
|
|
4539
|
+
depositValue = getStrictTokenValue(
|
|
4540
|
+
quoteTokenAmount,
|
|
4541
|
+
quoteSpotMarket.decimals,
|
|
4542
|
+
strictQuote
|
|
4543
|
+
);
|
|
4544
|
+
}
|
|
4545
|
+
calc.addIsolatedMarginCalculation(
|
|
4546
|
+
market.marketIndex,
|
|
4547
|
+
depositValue,
|
|
4548
|
+
positionUnrealizedPnl,
|
|
4549
|
+
worstCaseLiabilityValue,
|
|
4550
|
+
perpMarginRequirement
|
|
4551
|
+
);
|
|
4552
|
+
calc.addPerpLiabilityValue(worstCaseLiabilityValue);
|
|
4553
|
+
} else {
|
|
4554
|
+
// cross: add to global requirement and collateral
|
|
4555
|
+
calc.addCrossMarginRequirement(
|
|
4556
|
+
perpMarginRequirement,
|
|
4557
|
+
worstCaseLiabilityValue
|
|
4558
|
+
);
|
|
4559
|
+
calc.addCrossMarginTotalCollateral(positionUnrealizedPnl);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
return calc;
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
private isPerpPositionIsolated(perpPosition: PerpPosition): boolean {
|
|
4566
|
+
return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0;
|
|
4567
|
+
}
|
|
3918
4568
|
}
|