Skip to content

Commit 24caec7

Browse files
authored
lukas/update margin calc to match onchain (#2007)
* refactor: margin calc matches on chain func * fix: hadnle iso balance undefined * feat: margin calc tests * fix: lint and formatting * fix: logic to return with liq buffer * feat: add liq buffer in margin calc + cleaner canBeLiquidated return * fix: many fixes and testing script to bring cross collat margin calc parity * fix: linting errors * fix: some pr feedback, additional undefined checks + better margin logic * fix: better align margin calc with on chain + tests * feat: address pr nit feedback * rm: old user comparison script and old user margin calc * fix: dont call get margin calc twice getFreeCollateral * fix: margin calc equals method * fix: position flag bankruptcy wrong val
1 parent c85f7ed commit 24caec7

File tree

10 files changed

+1663
-93
lines changed

10 files changed

+1663
-93
lines changed

sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"build:browser": "yarn clean && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && node scripts/postbuild.js --force-env browser",
1717
"clean": "rm -rf lib",
1818
"test": "mocha -r ts-node/register tests/**/*.ts --ignore 'tests/dlob/**/*.ts'",
19+
"test:match": "mocha -r ts-node/register --ignore 'tests/dlob/**/*.ts'",
1920
"test:inspect": "mocha --inspect-brk -r ts-node/register tests/**/*.ts",
2021
"test:bignum": "mocha -r ts-node/register tests/bn/**/*.ts",
2122
"test:ci": "mocha -r ts-node/register tests/ci/**/*.ts",

sdk/src/marginCalculation.ts

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import { BN } from '@coral-xyz/anchor';
2+
import { MARGIN_PRECISION, ZERO } from './constants/numericConstants';
3+
import { getVariant, isVariant, MarketType } from './types';
4+
5+
export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill';
6+
7+
export type MarginCalculationMode =
8+
| { type: 'Standard' }
9+
| { type: 'Liquidation' };
10+
11+
export class MarketIdentifier {
12+
marketType: MarketType;
13+
marketIndex: number;
14+
15+
private constructor(marketType: MarketType, marketIndex: number) {
16+
this.marketType = marketType;
17+
this.marketIndex = marketIndex;
18+
}
19+
20+
static spot(marketIndex: number): MarketIdentifier {
21+
return new MarketIdentifier(MarketType.SPOT, marketIndex);
22+
}
23+
24+
static perp(marketIndex: number): MarketIdentifier {
25+
return new MarketIdentifier(MarketType.PERP, marketIndex);
26+
}
27+
28+
equals(other: MarketIdentifier | undefined): boolean {
29+
return (
30+
!!other &&
31+
isVariant(this.marketType, getVariant(other.marketType)) &&
32+
this.marketIndex === other.marketIndex
33+
);
34+
}
35+
}
36+
37+
export class MarginContext {
38+
marginType: MarginCategory;
39+
mode: MarginCalculationMode;
40+
strict: boolean;
41+
ignoreInvalidDepositOracles: boolean;
42+
isolatedMarginBuffers: Map<number, BN>;
43+
crossMarginBuffer: BN;
44+
45+
private constructor(marginType: MarginCategory) {
46+
this.marginType = marginType;
47+
this.mode = { type: 'Standard' };
48+
this.strict = false;
49+
this.ignoreInvalidDepositOracles = false;
50+
this.isolatedMarginBuffers = new Map();
51+
}
52+
53+
static standard(marginType: MarginCategory): MarginContext {
54+
return new MarginContext(marginType);
55+
}
56+
57+
static liquidation(
58+
crossMarginBuffer: BN,
59+
isolatedMarginBuffers: Map<number, BN>
60+
): MarginContext {
61+
const ctx = new MarginContext('Maintenance');
62+
ctx.mode = { type: 'Liquidation' };
63+
ctx.crossMarginBuffer = crossMarginBuffer;
64+
ctx.isolatedMarginBuffers = isolatedMarginBuffers;
65+
return ctx;
66+
}
67+
68+
strictMode(strict: boolean): this {
69+
this.strict = strict;
70+
return this;
71+
}
72+
73+
ignoreInvalidDeposits(ignore: boolean): this {
74+
this.ignoreInvalidDepositOracles = ignore;
75+
return this;
76+
}
77+
78+
setCrossMarginBuffer(crossMarginBuffer: BN): this {
79+
this.crossMarginBuffer = crossMarginBuffer;
80+
return this;
81+
}
82+
setIsolatedMarginBuffers(isolatedMarginBuffers: Map<number, BN>): this {
83+
this.isolatedMarginBuffers = isolatedMarginBuffers;
84+
return this;
85+
}
86+
setIsolatedMarginBuffer(marketIndex: number, isolatedMarginBuffer: BN): this {
87+
this.isolatedMarginBuffers.set(marketIndex, isolatedMarginBuffer);
88+
return this;
89+
}
90+
}
91+
92+
export class IsolatedMarginCalculation {
93+
marginRequirement: BN;
94+
totalCollateral: BN; // deposit + pnl
95+
totalCollateralBuffer: BN;
96+
marginRequirementPlusBuffer: BN;
97+
98+
constructor() {
99+
this.marginRequirement = ZERO;
100+
this.totalCollateral = ZERO;
101+
this.totalCollateralBuffer = ZERO;
102+
this.marginRequirementPlusBuffer = ZERO;
103+
}
104+
105+
getTotalCollateralPlusBuffer(): BN {
106+
return this.totalCollateral.add(this.totalCollateralBuffer);
107+
}
108+
109+
meetsMarginRequirement(): boolean {
110+
return this.totalCollateral.gte(this.marginRequirement);
111+
}
112+
113+
meetsMarginRequirementWithBuffer(): boolean {
114+
return this.getTotalCollateralPlusBuffer().gte(
115+
this.marginRequirementPlusBuffer
116+
);
117+
}
118+
119+
marginShortage(): BN {
120+
const shortage = this.marginRequirementPlusBuffer.sub(
121+
this.getTotalCollateralPlusBuffer()
122+
);
123+
return shortage.isNeg() ? ZERO : shortage;
124+
}
125+
}
126+
127+
export class MarginCalculation {
128+
context: MarginContext;
129+
totalCollateral: BN;
130+
totalCollateralBuffer: BN;
131+
marginRequirement: BN;
132+
marginRequirementPlusBuffer: BN;
133+
isolatedMarginCalculations: Map<number, IsolatedMarginCalculation>;
134+
allDepositOraclesValid: boolean;
135+
allLiabilityOraclesValid: boolean;
136+
withPerpIsolatedLiability: boolean;
137+
withSpotIsolatedLiability: boolean;
138+
totalPerpLiabilityValue: BN;
139+
trackedMarketMarginRequirement: BN;
140+
fuelDeposits: number;
141+
fuelBorrows: number;
142+
fuelPositions: number;
143+
144+
constructor(context: MarginContext) {
145+
this.context = context;
146+
this.totalCollateral = ZERO;
147+
this.totalCollateralBuffer = ZERO;
148+
this.marginRequirement = ZERO;
149+
this.marginRequirementPlusBuffer = ZERO;
150+
this.isolatedMarginCalculations = new Map();
151+
this.allDepositOraclesValid = true;
152+
this.allLiabilityOraclesValid = true;
153+
this.withPerpIsolatedLiability = false;
154+
this.withSpotIsolatedLiability = false;
155+
this.totalPerpLiabilityValue = ZERO;
156+
this.trackedMarketMarginRequirement = ZERO;
157+
this.fuelDeposits = 0;
158+
this.fuelBorrows = 0;
159+
this.fuelPositions = 0;
160+
}
161+
162+
addCrossMarginTotalCollateral(delta: BN): void {
163+
const crossMarginBuffer = this.context.crossMarginBuffer;
164+
this.totalCollateral = this.totalCollateral.add(delta);
165+
if (crossMarginBuffer.gt(ZERO) && delta.isNeg()) {
166+
this.totalCollateralBuffer = this.totalCollateralBuffer.add(
167+
delta.mul(crossMarginBuffer).div(MARGIN_PRECISION)
168+
);
169+
}
170+
}
171+
172+
addCrossMarginRequirement(marginRequirement: BN, liabilityValue: BN): void {
173+
const crossMarginBuffer = this.context.crossMarginBuffer;
174+
this.marginRequirement = this.marginRequirement.add(marginRequirement);
175+
if (crossMarginBuffer.gt(ZERO)) {
176+
this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add(
177+
marginRequirement.add(
178+
liabilityValue.mul(crossMarginBuffer).div(MARGIN_PRECISION)
179+
)
180+
);
181+
}
182+
}
183+
184+
addIsolatedMarginCalculation(
185+
marketIndex: number,
186+
depositValue: BN,
187+
pnl: BN,
188+
liabilityValue: BN,
189+
marginRequirement: BN
190+
): void {
191+
const totalCollateral = depositValue.add(pnl);
192+
const isolatedMarginBuffer =
193+
this.context.isolatedMarginBuffers.get(marketIndex) ?? ZERO;
194+
195+
const totalCollateralBuffer =
196+
isolatedMarginBuffer.gt(ZERO) && pnl.isNeg()
197+
? pnl.mul(isolatedMarginBuffer).div(MARGIN_PRECISION)
198+
: ZERO;
199+
200+
const marginRequirementPlusBuffer = isolatedMarginBuffer.gt(ZERO)
201+
? marginRequirement.add(
202+
liabilityValue.mul(isolatedMarginBuffer).div(MARGIN_PRECISION)
203+
)
204+
: marginRequirement;
205+
206+
const iso = new IsolatedMarginCalculation();
207+
iso.marginRequirement = marginRequirement;
208+
iso.totalCollateral = totalCollateral;
209+
iso.totalCollateralBuffer = totalCollateralBuffer;
210+
iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer;
211+
this.isolatedMarginCalculations.set(marketIndex, iso);
212+
}
213+
214+
addPerpLiabilityValue(perpLiabilityValue: BN): void {
215+
this.totalPerpLiabilityValue =
216+
this.totalPerpLiabilityValue.add(perpLiabilityValue);
217+
}
218+
219+
updateAllDepositOraclesValid(valid: boolean): void {
220+
this.allDepositOraclesValid = this.allDepositOraclesValid && valid;
221+
}
222+
223+
updateAllLiabilityOraclesValid(valid: boolean): void {
224+
this.allLiabilityOraclesValid = this.allLiabilityOraclesValid && valid;
225+
}
226+
227+
updateWithSpotIsolatedLiability(isolated: boolean): void {
228+
this.withSpotIsolatedLiability = this.withSpotIsolatedLiability || isolated;
229+
}
230+
231+
updateWithPerpIsolatedLiability(isolated: boolean): void {
232+
this.withPerpIsolatedLiability = this.withPerpIsolatedLiability || isolated;
233+
}
234+
235+
getCrossTotalCollateralPlusBuffer(): BN {
236+
return this.totalCollateral.add(this.totalCollateralBuffer);
237+
}
238+
239+
meetsCrossMarginRequirement(): boolean {
240+
return this.totalCollateral.gte(this.marginRequirement);
241+
}
242+
243+
meetsCrossMarginRequirementWithBuffer(): boolean {
244+
return this.getCrossTotalCollateralPlusBuffer().gte(
245+
this.marginRequirementPlusBuffer
246+
);
247+
}
248+
249+
meetsMarginRequirement(): boolean {
250+
if (!this.meetsCrossMarginRequirement()) return false;
251+
for (const [, iso] of this.isolatedMarginCalculations) {
252+
if (!iso.meetsMarginRequirement()) return false;
253+
}
254+
return true;
255+
}
256+
257+
meetsMarginRequirementWithBuffer(): boolean {
258+
if (!this.meetsCrossMarginRequirementWithBuffer()) return false;
259+
for (const [, iso] of this.isolatedMarginCalculations) {
260+
if (!iso.meetsMarginRequirementWithBuffer()) return false;
261+
}
262+
return true;
263+
}
264+
265+
getCrossFreeCollateral(): BN {
266+
const free = this.totalCollateral.sub(this.marginRequirement);
267+
return free.isNeg() ? ZERO : free;
268+
}
269+
270+
getIsolatedFreeCollateral(marketIndex: number): BN {
271+
const iso = this.isolatedMarginCalculations.get(marketIndex);
272+
if (!iso)
273+
throw new Error('InvalidMarginCalculation: missing isolated calc');
274+
const free = iso.totalCollateral.sub(iso.marginRequirement);
275+
return free.isNeg() ? ZERO : free;
276+
}
277+
278+
getIsolatedMarginCalculation(
279+
marketIndex: number
280+
): IsolatedMarginCalculation | undefined {
281+
return this.isolatedMarginCalculations.get(marketIndex);
282+
}
283+
284+
hasIsolatedMarginCalculation(marketIndex: number): boolean {
285+
return this.isolatedMarginCalculations.has(marketIndex);
286+
}
287+
}

sdk/src/math/margin.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,25 @@ export function calculateWorstCaseBaseAssetAmount(
165165
export function calculateWorstCasePerpLiabilityValue(
166166
perpPosition: PerpPosition,
167167
perpMarket: PerpMarketAccount,
168-
oraclePrice: BN
168+
oraclePrice: BN,
169+
includeOpenOrders = true
169170
): { worstCaseBaseAssetAmount: BN; worstCaseLiabilityValue: BN } {
171+
const isPredictionMarket = isVariant(perpMarket.contractType, 'prediction');
172+
173+
if (!includeOpenOrders) {
174+
return {
175+
worstCaseBaseAssetAmount: perpPosition.baseAssetAmount,
176+
worstCaseLiabilityValue: calculatePerpLiabilityValue(
177+
perpPosition.baseAssetAmount,
178+
oraclePrice,
179+
isPredictionMarket
180+
),
181+
};
182+
}
183+
170184
const allBids = perpPosition.baseAssetAmount.add(perpPosition.openBids);
171185
const allAsks = perpPosition.baseAssetAmount.add(perpPosition.openAsks);
172186

173-
const isPredictionMarket = isVariant(perpMarket.contractType, 'prediction');
174187
const allBidsLiabilityValue = calculatePerpLiabilityValue(
175188
allBids,
176189
oraclePrice,

sdk/src/math/spotPosition.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export function getWorstCaseTokenAmounts(
3333
spotMarketAccount: SpotMarketAccount,
3434
strictOraclePrice: StrictOraclePrice,
3535
marginCategory: MarginCategory,
36-
customMarginRatio?: number
36+
customMarginRatio?: number,
37+
includeOpenOrders: boolean = true
3738
): OrderFillSimulation {
3839
const tokenAmount = getSignedTokenAmount(
3940
getTokenAmount(
@@ -50,7 +51,10 @@ export function getWorstCaseTokenAmounts(
5051
strictOraclePrice
5152
);
5253

53-
if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) {
54+
if (
55+
(spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) ||
56+
!includeOpenOrders
57+
) {
5458
const { weight, weightedTokenValue } = calculateWeightedTokenValue(
5559
tokenAmount,
5660
tokenValue,

sdk/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,12 @@ export type PerpPosition = {
11411141
positionFlag: number;
11421142
};
11431143

1144+
export class PositionFlag {
1145+
static readonly IsolatedPosition = 1;
1146+
static readonly BeingLiquidated = 2;
1147+
static readonly Bankruptcy = 4;
1148+
}
1149+
11441150
export type UserStatsAccount = {
11451151
numberOfSubAccounts: number;
11461152
numberOfSubAccountsCreated: number;
@@ -1898,3 +1904,9 @@ export type CacheInfo = {
18981904
export type AmmCache = {
18991905
cache: CacheInfo[];
19001906
};
1907+
1908+
export type AccountLiquidatableStatus = {
1909+
canBeLiquidated: boolean;
1910+
marginRequirement: BN;
1911+
totalCollateral: BN;
1912+
};

0 commit comments

Comments
 (0)