@drift-labs/sdk
Version:
SDK for Drift Protocol
827 lines (716 loc) • 28.2 kB
text/typescript
import {
BN,
ZERO,
User,
UserAccount,
PublicKey,
PerpMarketAccount,
SpotMarketAccount,
PRICE_PRECISION,
OraclePriceData,
MMOraclePriceData,
BASE_PRECISION,
QUOTE_PRECISION,
calculatePositionPNL,
SPOT_MARKET_BALANCE_PRECISION,
getWorstCaseTokenAmounts,
StrictOraclePrice,
LAMPORTS_PRECISION,
SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION,
SpotBalanceType,
MARGIN_PRECISION,
getSpotAssetValue,
} from '../../src';
import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers';
import { assert } from '../../src/assert/assert';
import {
mockUserAccount,
makeMockUser as makeMockUserFromHelpers,
} from './helpers';
import * as _ from 'lodash';
async function makeMockUser(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
perpOraclePriceList,
spotOraclePriceList
): Promise<User> {
const umap = new MockUserMap();
const mockUser: User = await umap.mustGet('1');
mockUser._isSubscribed = true;
mockUser.driftClient._isSubscribed = true;
mockUser.driftClient.accountSubscriber.isSubscribed = true;
const oraclePriceMap = {};
// console.log(perpOraclePriceList, myMockPerpMarkets.length);
// console.log(spotOraclePriceList, myMockSpotMarkets.length);
for (let i = 0; i < myMockPerpMarkets.length; i++) {
oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] =
perpOraclePriceList[i];
}
for (let i = 0; i < myMockSpotMarkets.length; i++) {
oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] =
spotOraclePriceList[i];
}
// console.log('oraclePriceMap:', oraclePriceMap);
function getMockUserAccount(): UserAccount {
return myMockUserAccount;
}
function getMockPerpMarket(marketIndex): PerpMarketAccount {
return myMockPerpMarkets[marketIndex];
}
function getMockSpotMarket(marketIndex): SpotMarketAccount {
return myMockSpotMarkets[marketIndex];
}
function getMockOracle(oracleKey: PublicKey) {
// console.log('oracleKey.toString():', oracleKey.toString());
// console.log(
// 'oraclePriceMap[oracleKey.toString()]:',
// oraclePriceMap[oracleKey.toString()]
// );
const QUOTE_ORACLE_PRICE_DATA: OraclePriceData = {
price: new BN(
oraclePriceMap[oracleKey.toString()] * PRICE_PRECISION.toNumber()
),
slot: new BN(0),
confidence: new BN(1),
hasSufficientNumberOfDataPoints: true,
};
return {
data: QUOTE_ORACLE_PRICE_DATA,
slot: 0,
};
}
function getOracleDataForPerpMarket(marketIndex) {
const oracle = getMockPerpMarket(marketIndex).amm.oracle;
return getMockOracle(oracle).data;
}
function getOracleDataForSpotMarket(marketIndex) {
const oracle = getMockSpotMarket(marketIndex).oracle;
return getMockOracle(oracle).data;
}
function getMMOracleDataForPerpMarket(
marketIndex: number
): MMOraclePriceData {
const oracle = getMockPerpMarket(marketIndex).amm.oracle;
return getMockOracle(oracle).data as unknown as MMOraclePriceData;
}
mockUser.getUserAccount = getMockUserAccount;
mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket;
mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket;
mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle;
mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket;
mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket;
mockUser.driftClient.getMMOracleDataForPerpMarket =
getMMOracleDataForPerpMarket;
return mockUser;
}
describe('User Tests', () => {
it('empty user account', async () => {
console.log(mockSpotMarkets[0]);
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
console.log(
'spot cumulativeDepositInterest:',
mockSpotMarkets[0].cumulativeDepositInterest.toString()
);
const mockUser: User = await makeMockUser(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const uA = mockUser.getUserAccount();
assert(uA.idle == false);
console.log(
'spot cumulativeDepositInterest:',
myMockSpotMarkets[0].cumulativeDepositInterest.toString()
);
assert(mockUser.getFreeCollateral().eq(ZERO));
console.log(mockUser.getHealth());
assert(mockUser.getHealth() == 100);
console.log(mockUser.getMaxLeverageForPerp(0));
assert(mockUser.getMaxLeverageForPerp(0).eq(ZERO));
});
it('user account unsettled pnl', async () => {
// no collateral, but positive upnl no liability
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(
0 * BASE_PRECISION.toNumber()
);
myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(
10 * QUOTE_PRECISION.toNumber()
);
assert(
myMockUserAccount.perpPositions[0].quoteAssetAmount.eq(new BN('10000000'))
); // $10
const mockUser: User = await makeMockUser(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const uA = mockUser.getUserAccount();
assert(uA.idle == false);
const activePerps = mockUser.getActivePerpPositions();
assert(activePerps.length == 1);
assert(uA.perpPositions[0].quoteAssetAmount.eq(new BN('10000000'))); // $10
assert(mockUser.getFreeCollateral().eq(ZERO));
const quotePrice = mockUser.driftClient.getOracleDataForSpotMarket(0).price;
console.log('quotePrice:', quotePrice.toString());
assert(quotePrice.eq(new BN('1000000')));
const pnl1 = calculatePositionPNL(
myMockPerpMarkets[0],
activePerps[0],
false,
mockUser.driftClient.getOracleDataForPerpMarket(0)
);
console.log('pnl1:', pnl1.toString());
assert(pnl1.eq(new BN('10000000')));
const upnl = mockUser.getUnrealizedPNL(false, undefined, undefined, false);
console.log('upnl:', upnl.toString());
assert(upnl.eq(new BN('10000000'))); // $10
const liqResult = mockUser.canBeLiquidated();
console.log(liqResult);
assert(liqResult.canBeLiquidated == false);
assert(liqResult.marginRequirement.eq(ZERO));
assert(liqResult.totalCollateral.eq(ZERO));
console.log(mockUser.getHealth());
assert(mockUser.getHealth() == 100);
console.log(mockUser.getMaxLeverageForPerp(0));
assert(mockUser.getMaxLeverageForPerp(0).eq(ZERO));
});
it('liquidatable long user account', async () => {
// no collateral, but positive upnl w/ liability
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(
20 * BASE_PRECISION.toNumber()
);
myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(
-10 * QUOTE_PRECISION.toNumber()
);
const mockUser: User = await makeMockUser(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const uA = mockUser.getUserAccount();
assert(uA.idle == false);
assert(mockUser.getFreeCollateral().eq(ZERO));
const upnl = mockUser.getUnrealizedPNL(true, 0, undefined, false);
console.log('upnl:', upnl.toString());
assert(upnl.eq(new BN('10000000'))); // $10
const liqResult = mockUser.canBeLiquidated();
console.log(liqResult);
assert(liqResult.canBeLiquidated == true);
assert(liqResult.marginRequirement.eq(new BN('2000000'))); //10x maint leverage
assert(liqResult.totalCollateral.eq(ZERO));
console.log(mockUser.getHealth());
assert(mockUser.getHealth() == 0);
console.log(mockUser.getMaxLeverageForPerp(0));
assert(mockUser.getMaxLeverageForPerp(0).eq(new BN('20000')));
});
it('large usdc user account', async () => {
// no collateral, but positive upnl w/ liability
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockPerpMarkets[0].imfFactor = 550;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(
10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber()
); //10k
const mockUser: User = await makeMockUser(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const uA = mockUser.getUserAccount();
assert(uA.idle == false);
assert(uA.perpPositions[0].baseAssetAmount.eq(ZERO));
assert(uA.perpPositions[0].quoteAssetAmount.eq(ZERO));
assert(mockUser.getActivePerpPositions().length == 0);
assert(
uA.spotPositions[0].scaledBalance.eq(
new BN(10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber())
)
);
for (let i = 1; i < 8; i++) {
assert(uA.spotPositions[i].scaledBalance.eq(ZERO));
}
console.log(
'mockUser.getTokenAmount():',
mockUser.getTokenAmount(0).toString()
);
console.log(
'spot cumulativeDepositInterest:',
mockSpotMarkets[0].cumulativeDepositInterest.toString()
);
const expectedAmount = new BN('10000000000');
assert(mockUser.getTokenAmount(0).eq(expectedAmount));
assert(mockUser.getNetSpotMarketValue().eq(expectedAmount));
assert(
mockUser
.getSpotMarketAssetAndLiabilityValue()
.totalLiabilityValue.eq(ZERO)
);
assert(mockUser.getFreeCollateral().gt(ZERO));
const upnl = mockUser.getUnrealizedPNL(true, 0, undefined, false);
console.log('upnl:', upnl.toString());
assert(upnl.eq(new BN('0'))); // $10
const liqResult = mockUser.canBeLiquidated();
console.log(liqResult);
assert(liqResult.canBeLiquidated == false);
assert(liqResult.marginRequirement.eq(new BN('0'))); //10x maint leverage
assert(liqResult.totalCollateral.eq(expectedAmount));
console.log(mockUser.getHealth());
assert(mockUser.getHealth() == 100);
console.log(
'getMaxLeverageForPerp:',
mockUser.getMaxLeverageForPerp(0).toString()
);
assert(mockUser.getMaxLeverageForPerp(0).eq(new BN('37358'))); // ~3.7x
assert(
mockUser.getMaxLeverageForPerp(0, 'Maintenance').eq(new BN('37358'))
); // same (marginCategory unused)
});
it('worst case token amount', async () => {
const myMockUserAccount = _.cloneDeep(mockUserAccount);
const solMarket = Object.assign({}, _.cloneDeep(mockSpotMarkets[1]), {
initialAssetWeight: 8000,
initialLiabilityWeight: 12000,
cumulativeDepositInterest: SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION,
cumulativeBorrowInterest: SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION,
});
const strictOraclePrice = new StrictOraclePrice(PRICE_PRECISION.muln(100));
let spotPosition = Object.assign({}, myMockUserAccount.spotPositions[1], {
marketIndex: 1,
openBids: new BN(100).mul(LAMPORTS_PRECISION),
});
let worstCase = getWorstCaseTokenAmounts(
spotPosition,
solMarket,
strictOraclePrice,
'Initial'
);
assert(worstCase.tokenAmount.eq(new BN(100).mul(LAMPORTS_PRECISION))); // 100
assert(worstCase.tokenValue.eq(new BN(10000).mul(PRICE_PRECISION))); // $10k
assert(worstCase.weightedTokenValue.eq(new BN(8000).mul(PRICE_PRECISION))); // $8k
assert(worstCase.ordersValue.eq(new BN(-10000).mul(PRICE_PRECISION))); // -$10k
assert(
worstCase.freeCollateralContribution.eq(
new BN(-2000).mul(QUOTE_PRECISION)
)
); // -$2k
spotPosition = Object.assign({}, myMockUserAccount.spotPositions[1], {
marketIndex: 1,
scaledBalance: new BN(100).mul(SPOT_MARKET_BALANCE_PRECISION),
openBids: new BN(100).mul(LAMPORTS_PRECISION),
});
worstCase = getWorstCaseTokenAmounts(
spotPosition,
solMarket,
strictOraclePrice,
'Initial'
);
assert(worstCase.tokenAmount.eq(new BN(200).mul(LAMPORTS_PRECISION))); // 200
assert(worstCase.tokenValue.eq(new BN(20000).mul(PRICE_PRECISION))); // $20k
assert(worstCase.weightedTokenValue.eq(new BN(16000).mul(PRICE_PRECISION))); // $16k
assert(worstCase.ordersValue.eq(new BN(-10000).mul(PRICE_PRECISION))); // -$10k
assert(
worstCase.freeCollateralContribution.eq(new BN(6000).mul(QUOTE_PRECISION))
); // $6k
spotPosition = Object.assign({}, myMockUserAccount.spotPositions[1], {
marketIndex: 1,
openAsks: new BN(-100).mul(LAMPORTS_PRECISION),
});
worstCase = getWorstCaseTokenAmounts(
spotPosition,
solMarket,
strictOraclePrice,
'Initial'
);
assert(worstCase.tokenAmount.eq(new BN(-100).mul(LAMPORTS_PRECISION)));
assert(worstCase.tokenValue.eq(new BN(-10000).mul(PRICE_PRECISION))); // -$10k
assert(
worstCase.weightedTokenValue.eq(new BN(-12000).mul(PRICE_PRECISION))
); // -$12k
assert(worstCase.ordersValue.eq(new BN(10000).mul(PRICE_PRECISION))); // $10k
assert(
worstCase.freeCollateralContribution.eq(
new BN(-2000).mul(QUOTE_PRECISION)
)
); // -$2k
spotPosition = Object.assign({}, myMockUserAccount.spotPositions[1], {
marketIndex: 1,
balanceType: SpotBalanceType.BORROW,
scaledBalance: new BN(100).mul(SPOT_MARKET_BALANCE_PRECISION),
openAsks: new BN(-100).mul(LAMPORTS_PRECISION),
});
worstCase = getWorstCaseTokenAmounts(
spotPosition,
solMarket,
strictOraclePrice,
'Initial'
);
assert(worstCase.tokenAmount.eq(new BN(-200).mul(LAMPORTS_PRECISION)));
assert(worstCase.tokenValue.eq(new BN(-20000).mul(PRICE_PRECISION))); // -$20k
assert(
worstCase.weightedTokenValue.eq(new BN(-24000).mul(PRICE_PRECISION))
); // -$24k
assert(worstCase.ordersValue.eq(new BN(10000).mul(PRICE_PRECISION))); // $10k
assert(
worstCase.freeCollateralContribution.eq(
new BN(-14000).mul(QUOTE_PRECISION)
)
); // -$2k
});
it('custom margin ratio (sol spot)', async () => {
const myMockUserAccount = _.cloneDeep(mockUserAccount);
const solMarket = Object.assign({}, _.cloneDeep(mockSpotMarkets[1]), {
initialAssetWeight: 8000,
initialLiabilityWeight: 12000,
cumulativeDepositInterest: SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION,
cumulativeBorrowInterest: SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION,
});
// $25
const strictOraclePrice = new StrictOraclePrice(PRICE_PRECISION.muln(25));
const spotPosition = Object.assign({}, myMockUserAccount.spotPositions[1], {
marketIndex: 1,
openBids: new BN(100).mul(LAMPORTS_PRECISION),
});
const worstCase = getWorstCaseTokenAmounts(
spotPosition,
solMarket,
strictOraclePrice,
'Initial',
myMockUserAccount.maxMarginRatio
);
console.log(worstCase);
assert(worstCase.weight.eq(new BN(8000)));
myMockUserAccount.maxMarginRatio = MARGIN_PRECISION.toNumber(); // max 1x pls
const worstCaseAfter = getWorstCaseTokenAmounts(
spotPosition,
solMarket,
strictOraclePrice,
'Initial',
myMockUserAccount.maxMarginRatio
);
console.log(worstCaseAfter);
assert(worstCaseAfter.weight.eq(new BN(0))); // not allowed to increase exposure
// customMarginRatio > SPOT weight precision must not throw (BN subn asserted non-negative)
const depositOnlyNonQuote = Object.assign(
{},
myMockUserAccount.spotPositions[1],
{
marketIndex: 1,
scaledBalance: new BN(100).mul(SPOT_MARKET_BALANCE_PRECISION),
openBids: ZERO,
openAsks: ZERO,
}
);
for (const ratio of [
MARGIN_PRECISION.toNumber() * 2,
MARGIN_PRECISION.toNumber() * 10,
89478485,
]) {
const wc = getWorstCaseTokenAmounts(
depositOnlyNonQuote,
solMarket,
strictOraclePrice,
'Initial',
ratio
);
assert(wc.weight.eq(new BN(0)));
assert(wc.weightedTokenValue.eq(ZERO));
}
});
it('getSpotAssetValue: large maxMarginRatio does not throw (matches getFreeCollateral path)', () => {
const solMarket = Object.assign({}, _.cloneDeep(mockSpotMarkets[1]), {
marketIndex: 1,
initialAssetWeight: 8000,
initialLiabilityWeight: 12000,
cumulativeDepositInterest: SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION,
cumulativeBorrowInterest: SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION,
});
const strictOraclePrice = new StrictOraclePrice(PRICE_PRECISION.muln(25));
const tokenAmount = new BN(100).mul(LAMPORTS_PRECISION);
for (const ratio of [MARGIN_PRECISION.toNumber() * 2, 89478485]) {
const assetValue = getSpotAssetValue(
tokenAmount,
strictOraclePrice,
solMarket,
ratio,
'Initial'
);
assert(assetValue.eq(ZERO));
}
});
it('custom margin ratio (sol perp)', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// myMockPerpMarkets[0].imfFactor = 550;
myMockPerpMarkets[0].marginRatioInitial = 2000; // 5x
myMockPerpMarkets[0].marginRatioMaintenance = 1000; // 10x
myMockSpotMarkets[0].initialAssetWeight = 1000;
myMockSpotMarkets[0].initialLiabilityWeight = 1000;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(
10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber()
); //10k
const mockUser: User = await makeMockUser(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
assert(mockUser.getTokenAmount(0).eq(new BN('10000000000')));
assert(mockUser.getNetSpotMarketValue().eq(new BN('10000000000')));
assert(
mockUser
.getSpotMarketAssetAndLiabilityValue()
.totalLiabilityValue.eq(ZERO)
);
assert(mockUser.getFreeCollateral().gt(ZERO));
let iLev = mockUser.getMaxLeverageForPerp(0, 'Initial').toNumber();
let mLev = mockUser.getMaxLeverageForPerp(0, 'Maintenance').toNumber();
console.log(iLev, mLev);
assert(iLev == 5000);
assert(mLev == 5000);
myMockUserAccount.maxMarginRatio = MARGIN_PRECISION.div(
new BN(2)
).toNumber(); // 2x max pls
const mockUser2: User = await makeMockUser(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
iLev = mockUser2.getMaxLeverageForPerp(0, 'Initial').toNumber();
mLev = mockUser2.getMaxLeverageForPerp(0, 'Maintenance').toNumber();
console.log(iLev, mLev);
assert(iLev == 2000);
assert(mLev == 2000);
});
it('getTotalIsolatedPositionDeposits sums isolated USDC deposits', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// Give perp position 0 an isolated deposit of 100 USDC
// mockSpotMarkets[0] is USDC with cumulativeDepositInterest = SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION
// so scaledBalance of 100 * SPOT_MARKET_BALANCE_PRECISION = 100 USDC token amount
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(1); // make position active
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
// Give perp position 1 an isolated deposit of 50 USDC
myMockUserAccount.perpPositions[1].marketIndex = 1;
myMockUserAccount.perpPositions[1].baseAssetAmount = new BN(1);
myMockUserAccount.perpPositions[1].isolatedPositionScaledBalance = new BN(
50
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const totalIsolatedDeposits = mockUser.getTotalIsolatedPositionDeposits();
// 150 USDC = 150 * QUOTE_PRECISION
assert(totalIsolatedDeposits.eq(new BN(150).mul(QUOTE_PRECISION)));
});
it('getTotalIsolatedPositionDeposits applies oracle price for depeg', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// Give spot market 0 (USDC) a unique oracle so it gets its own price
// (all mock markets share PublicKey.default, causing oracle map overwrites)
const usdcOracle = new PublicKey(
'Erq8cpkof3kitj7rkzKba3j1Hdib6gFFZ7QktwGpsa3w'
);
myMockSpotMarkets[0].oracle = usdcOracle;
// 100 USDC isolated deposit on perp position 0
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(1);
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
// Spot oracle price list: index 0 = USDC at $0.99 (depeg)
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[0.99, 1, 1, 1, 1, 1, 1, 1]
);
const totalIsolatedDeposits = mockUser.getTotalIsolatedPositionDeposits();
// 100 tokens * $0.99 = $99 = 99 * QUOTE_PRECISION
assert(totalIsolatedDeposits.eq(new BN(99).mul(QUOTE_PRECISION)));
});
it('getNetUsdValue includes isolated position deposits', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// 200 USDC cross-margin deposit in spot position 0 (USDC market)
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(200).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
// 100 USDC isolated deposit on perp position 0
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION; // 1 unit long
myMockUserAccount.perpPositions[0].quoteAssetAmount = QUOTE_PRECISION.neg(); // entered at $1, PnL=0
myMockUserAccount.perpPositions[0].quoteEntryAmount = QUOTE_PRECISION.neg(); // entered at $1
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const netUsdValue = mockUser.getNetUsdValue();
// Cross spot: 200 USDC + Isolated deposit: 100 USDC + PnL: 0 = 300 USDC
assert(netUsdValue.eq(new BN(300).mul(QUOTE_PRECISION)));
});
it('getTotalAssetValue includes isolated position deposits', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// 200 USDC cross-margin deposit
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(200).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
// 100 USDC isolated deposit on perp position 0
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteAssetAmount = QUOTE_PRECISION.neg(); // PnL=0 at oracle $1
myMockUserAccount.perpPositions[0].quoteEntryAmount = QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const totalAssetValue = mockUser.getTotalAssetValue();
// Cross spot asset: 200 USDC + Isolated: 100 USDC + PnL: 0 = 300 USDC
assert(totalAssetValue.eq(new BN(300).mul(QUOTE_PRECISION)));
});
it('getTotalAssetValue with Initial margin excludes isolated position deposits', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// 200 USDC cross-margin deposit
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(200).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
// 100 USDC isolated deposit on perp position 0
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteAssetAmount = QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteEntryAmount = QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const totalAssetValue = mockUser.getTotalAssetValue('Initial');
// Cross spot asset only: 200 USDC. Isolated collateral is handled separately.
assert(totalAssetValue.eq(new BN(200).mul(QUOTE_PRECISION)));
});
it('getLeverageComponents aggregate path includes isolated deposits in spotAssetValue', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// 200 USDC cross-margin deposit
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(200).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
// 100 USDC isolated deposit on perp position 0 with 1 unit long at $1
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteEntryAmount = QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const { spotAssetValue } = mockUser.getLeverageComponents();
// Cross spot: 200 USDC + Isolated: 100 USDC = 300 USDC
assert(spotAssetValue.eq(new BN(300).mul(QUOTE_PRECISION)));
});
it('getLeverageComponents with Initial margin excludes isolated deposits from aggregate spotAssetValue', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
// 200 USDC cross-margin deposit
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(200).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
// 100 USDC isolated deposit on perp position 0 with 1 unit long at $1
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteEntryAmount = QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
);
const { spotAssetValue } = mockUser.getLeverageComponents(true, 'Initial');
// Cross spot asset only: 200 USDC. Isolated collateral is handled separately.
assert(spotAssetValue.eq(new BN(200).mul(QUOTE_PRECISION)));
});
});