@dainprotocol/drift-sdk
Version:
SDK for Drift Protocol
442 lines (441 loc) • 18.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.uncrossL2 = exports.groupL2 = exports.getVammL2Generator = exports.createL2Levels = exports.mergeL2LevelGenerators = exports.getL2GeneratorFromDLOBNodes = exports.DEFAULT_TOP_OF_BOOK_QUOTE_AMOUNTS = void 0;
const __1 = require("..");
const assert_1 = require("../assert/assert");
exports.DEFAULT_TOP_OF_BOOK_QUOTE_AMOUNTS = [
new __1.BN(500).mul(__1.QUOTE_PRECISION),
new __1.BN(1000).mul(__1.QUOTE_PRECISION),
new __1.BN(2000).mul(__1.QUOTE_PRECISION),
new __1.BN(5000).mul(__1.QUOTE_PRECISION),
];
/**
* Get an {@link Generator<L2Level>} generator from a {@link Generator<DLOBNode>}
* @param dlobNodes e.g. {@link DLOB#getRestingLimitAsks} or {@link DLOB#getRestingLimitBids}
* @param oraclePriceData
* @param slot
*/
const INDICATIVE_QUOTES_PUBKEY = 'inDNdu3ML4vG5LNExqcwuCQtLcCU8KfK5YM2qYV3JJz';
function* getL2GeneratorFromDLOBNodes(dlobNodes, oraclePriceData, slot) {
for (const dlobNode of dlobNodes) {
const size = dlobNode.order.baseAssetAmount.sub(dlobNode.order.baseAssetAmountFilled);
yield {
size,
price: dlobNode.getPrice(oraclePriceData, slot),
sources: dlobNode.userAccount == INDICATIVE_QUOTES_PUBKEY
? { indicative: size }
: {
dlob: size,
},
};
}
}
exports.getL2GeneratorFromDLOBNodes = getL2GeneratorFromDLOBNodes;
function* mergeL2LevelGenerators(l2LevelGenerators, compare) {
const generators = l2LevelGenerators.map((generator) => {
return {
generator,
next: generator.next(),
};
});
let next;
do {
next = generators.reduce((best, next) => {
if (next.next.done) {
return best;
}
if (!best) {
return next;
}
if (compare(next.next.value, best.next.value)) {
return next;
}
else {
return best;
}
}, undefined);
if (next) {
yield next.next.value;
next.next = next.generator.next();
}
} while (next !== undefined);
}
exports.mergeL2LevelGenerators = mergeL2LevelGenerators;
function createL2Levels(generator, depth) {
const levels = [];
for (const level of generator) {
const price = level.price;
const size = level.size;
if (levels.length > 0 && levels[levels.length - 1].price.eq(price)) {
const currentLevel = levels[levels.length - 1];
currentLevel.size = currentLevel.size.add(size);
for (const [source, size] of Object.entries(level.sources)) {
if (currentLevel.sources[source]) {
currentLevel.sources[source] = currentLevel.sources[source].add(size);
}
else {
currentLevel.sources[source] = size;
}
}
}
else if (levels.length === depth) {
break;
}
else {
levels.push(level);
}
}
return levels;
}
exports.createL2Levels = createL2Levels;
function getVammL2Generator({ marketAccount, oraclePriceData, numOrders, now, topOfBookQuoteAmounts, }) {
let numBaseOrders = numOrders;
if (topOfBookQuoteAmounts) {
numBaseOrders = numOrders - topOfBookQuoteAmounts.length;
(0, assert_1.assert)(topOfBookQuoteAmounts.length < numOrders);
}
const updatedAmm = (0, __1.calculateUpdatedAMM)(marketAccount.amm, oraclePriceData);
const vammFillsDisabled = (0, __1.isOperationPaused)(marketAccount.pausedOperations, __1.PerpOperation.AMM_FILL);
let [openBids, openAsks] = vammFillsDisabled
? [__1.ZERO, __1.ZERO]
: (0, __1.calculateMarketOpenBidAsk)(updatedAmm.baseAssetReserve, updatedAmm.minBaseAssetReserve, updatedAmm.maxBaseAssetReserve, updatedAmm.orderStepSize);
const minOrderSize = marketAccount.amm.minOrderSize;
if (openBids.lt(minOrderSize.muln(2))) {
openBids = __1.ZERO;
}
if (openAsks.abs().lt(minOrderSize.muln(2))) {
openAsks = __1.ZERO;
}
now = now !== null && now !== void 0 ? now : new __1.BN(Date.now() / 1000);
const [bidReserves, askReserves] = (0, __1.calculateSpreadReserves)(updatedAmm, oraclePriceData, now, (0, __1.isVariant)(marketAccount.contractType, 'prediction'));
let numBids = 0;
let topOfBookBidSize = __1.ZERO;
let bidSize = openBids.div(new __1.BN(numBaseOrders));
const bidAmm = {
baseAssetReserve: bidReserves.baseAssetReserve,
quoteAssetReserve: bidReserves.quoteAssetReserve,
sqrtK: updatedAmm.sqrtK,
pegMultiplier: updatedAmm.pegMultiplier,
};
const getL2Bids = function* () {
while (numBids < numOrders && bidSize.gt(__1.ZERO)) {
let quoteSwapped = __1.ZERO;
let baseSwapped = __1.ZERO;
let [afterSwapQuoteReserves, afterSwapBaseReserves] = [__1.ZERO, __1.ZERO];
if (topOfBookQuoteAmounts && numBids < (topOfBookQuoteAmounts === null || topOfBookQuoteAmounts === void 0 ? void 0 : topOfBookQuoteAmounts.length)) {
const remainingBaseLiquidity = openBids.sub(topOfBookBidSize);
quoteSwapped = topOfBookQuoteAmounts[numBids];
[afterSwapQuoteReserves, afterSwapBaseReserves] =
(0, __1.calculateAmmReservesAfterSwap)(bidAmm, 'quote', quoteSwapped, __1.SwapDirection.REMOVE);
baseSwapped = bidAmm.baseAssetReserve.sub(afterSwapBaseReserves).abs();
if (baseSwapped.eq(__1.ZERO)) {
return;
}
if (remainingBaseLiquidity.lt(baseSwapped)) {
baseSwapped = remainingBaseLiquidity;
[afterSwapQuoteReserves, afterSwapBaseReserves] =
(0, __1.calculateAmmReservesAfterSwap)(bidAmm, 'base', baseSwapped, __1.SwapDirection.ADD);
quoteSwapped = (0, __1.calculateQuoteAssetAmountSwapped)(bidAmm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(), bidAmm.pegMultiplier, __1.SwapDirection.ADD);
}
topOfBookBidSize = topOfBookBidSize.add(baseSwapped);
bidSize = openBids.sub(topOfBookBidSize).div(new __1.BN(numBaseOrders));
}
else {
baseSwapped = bidSize;
[afterSwapQuoteReserves, afterSwapBaseReserves] =
(0, __1.calculateAmmReservesAfterSwap)(bidAmm, 'base', baseSwapped, __1.SwapDirection.ADD);
quoteSwapped = (0, __1.calculateQuoteAssetAmountSwapped)(bidAmm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(), bidAmm.pegMultiplier, __1.SwapDirection.ADD);
}
const price = quoteSwapped.mul(__1.BASE_PRECISION).div(baseSwapped);
bidAmm.baseAssetReserve = afterSwapBaseReserves;
bidAmm.quoteAssetReserve = afterSwapQuoteReserves;
yield {
price,
size: baseSwapped,
sources: { vamm: baseSwapped },
};
numBids++;
}
};
let numAsks = 0;
let topOfBookAskSize = __1.ZERO;
let askSize = openAsks.abs().div(new __1.BN(numBaseOrders));
const askAmm = {
baseAssetReserve: askReserves.baseAssetReserve,
quoteAssetReserve: askReserves.quoteAssetReserve,
sqrtK: updatedAmm.sqrtK,
pegMultiplier: updatedAmm.pegMultiplier,
};
const getL2Asks = function* () {
while (numAsks < numOrders && askSize.gt(__1.ZERO)) {
let quoteSwapped = __1.ZERO;
let baseSwapped = __1.ZERO;
let [afterSwapQuoteReserves, afterSwapBaseReserves] = [__1.ZERO, __1.ZERO];
if (topOfBookQuoteAmounts && numAsks < (topOfBookQuoteAmounts === null || topOfBookQuoteAmounts === void 0 ? void 0 : topOfBookQuoteAmounts.length)) {
const remainingBaseLiquidity = openAsks
.mul(new __1.BN(-1))
.sub(topOfBookAskSize);
quoteSwapped = topOfBookQuoteAmounts[numAsks];
[afterSwapQuoteReserves, afterSwapBaseReserves] =
(0, __1.calculateAmmReservesAfterSwap)(askAmm, 'quote', quoteSwapped, __1.SwapDirection.ADD);
baseSwapped = askAmm.baseAssetReserve.sub(afterSwapBaseReserves).abs();
if (baseSwapped.eq(__1.ZERO)) {
return;
}
if (remainingBaseLiquidity.lt(baseSwapped)) {
baseSwapped = remainingBaseLiquidity;
[afterSwapQuoteReserves, afterSwapBaseReserves] =
(0, __1.calculateAmmReservesAfterSwap)(askAmm, 'base', baseSwapped, __1.SwapDirection.REMOVE);
quoteSwapped = (0, __1.calculateQuoteAssetAmountSwapped)(askAmm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(), askAmm.pegMultiplier, __1.SwapDirection.REMOVE);
}
topOfBookAskSize = topOfBookAskSize.add(baseSwapped);
askSize = openAsks
.abs()
.sub(topOfBookAskSize)
.div(new __1.BN(numBaseOrders));
}
else {
baseSwapped = askSize;
[afterSwapQuoteReserves, afterSwapBaseReserves] =
(0, __1.calculateAmmReservesAfterSwap)(askAmm, 'base', askSize, __1.SwapDirection.REMOVE);
quoteSwapped = (0, __1.calculateQuoteAssetAmountSwapped)(askAmm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(), askAmm.pegMultiplier, __1.SwapDirection.REMOVE);
}
const price = quoteSwapped.mul(__1.BASE_PRECISION).div(baseSwapped);
askAmm.baseAssetReserve = afterSwapBaseReserves;
askAmm.quoteAssetReserve = afterSwapQuoteReserves;
yield {
price,
size: baseSwapped,
sources: { vamm: baseSwapped },
};
numAsks++;
}
};
return {
getL2Bids,
getL2Asks,
};
}
exports.getVammL2Generator = getVammL2Generator;
function groupL2(l2, grouping, depth) {
return {
bids: groupL2Levels(l2.bids, grouping, __1.PositionDirection.LONG, depth),
asks: groupL2Levels(l2.asks, grouping, __1.PositionDirection.SHORT, depth),
slot: l2.slot,
};
}
exports.groupL2 = groupL2;
function cloneL2Level(level) {
if (!level)
return level;
return {
price: level.price,
size: level.size,
sources: { ...level.sources },
};
}
function groupL2Levels(levels, grouping, direction, depth) {
const groupedLevels = [];
for (const level of levels) {
const price = (0, __1.standardizePrice)(level.price, grouping, direction);
const size = level.size;
if (groupedLevels.length > 0 &&
groupedLevels[groupedLevels.length - 1].price.eq(price)) {
// Clones things so we don't mutate the original
const currentLevel = cloneL2Level(groupedLevels[groupedLevels.length - 1]);
currentLevel.size = currentLevel.size.add(size);
for (const [source, size] of Object.entries(level.sources)) {
if (currentLevel.sources[source]) {
currentLevel.sources[source] = currentLevel.sources[source].add(size);
}
else {
currentLevel.sources[source] = size;
}
}
groupedLevels[groupedLevels.length - 1] = currentLevel;
}
else {
const groupedLevel = {
price: price,
size,
sources: level.sources,
};
groupedLevels.push(groupedLevel);
}
if (groupedLevels.length === depth) {
break;
}
}
return groupedLevels;
}
/**
* Method to merge bids or asks by price
*/
const mergeByPrice = (bidsOrAsks) => {
const merged = new Map();
for (const level of bidsOrAsks) {
const key = level.price.toString();
if (merged.has(key)) {
const existing = merged.get(key);
existing.size = existing.size.add(level.size);
for (const [source, size] of Object.entries(level.sources)) {
if (existing.sources[source]) {
existing.sources[source] = existing.sources[source].add(size);
}
else {
existing.sources[source] = size;
}
}
}
else {
merged.set(key, cloneL2Level(level));
}
}
return Array.from(merged.values());
};
/**
* The purpose of this function is uncross the L2 orderbook by modifying the bid/ask price at the top of the book
* This will make the liquidity look worse but more intuitive (users familiar with clob get confused w temporarily
* crossing book)
*
* Things to note about how it works:
* - it will not uncross the user's liquidity
* - it does the uncrossing by "shifting" the crossing liquidity to the nearest uncrossed levels. Thus the output liquidity maintains the same total size.
*
* @param bids
* @param asks
* @param oraclePrice
* @param oracleTwap5Min
* @param markTwap5Min
* @param grouping
* @param userBids
* @param userAsks
*/
function uncrossL2(bids, asks, oraclePrice, oracleTwap5Min, markTwap5Min, grouping, userBids, userAsks) {
// If there are no bids or asks, there is nothing to center
if (bids.length === 0 || asks.length === 0) {
return { bids, asks };
}
// If the top of the book is already centered, there is nothing to do
if (bids[0].price.lt(asks[0].price)) {
return { bids, asks };
}
const newBids = [];
const newAsks = [];
const updateLevels = (newPrice, oldLevel, levels) => {
if (levels.length > 0 && levels[levels.length - 1].price.eq(newPrice)) {
levels[levels.length - 1].size = levels[levels.length - 1].size.add(oldLevel.size);
for (const [source, size] of Object.entries(oldLevel.sources)) {
if (levels[levels.length - 1].sources[source]) {
levels[levels.length - 1].sources = {
...levels[levels.length - 1].sources,
[source]: levels[levels.length - 1].sources[source].add(size),
};
}
else {
levels[levels.length - 1].sources[source] = size;
}
}
}
else {
levels.push({
price: newPrice,
size: oldLevel.size,
sources: oldLevel.sources,
});
}
};
// This is the best estimate of the premium in the market vs oracle to filter crossing around
const referencePrice = oraclePrice.add(markTwap5Min.sub(oracleTwap5Min));
let bidIndex = 0;
let askIndex = 0;
let maxBid;
let minAsk;
const getPriceAndSetBound = (newPrice, direction) => {
if ((0, __1.isVariant)(direction, 'long')) {
maxBid = maxBid ? __1.BN.min(maxBid, newPrice) : newPrice;
return maxBid;
}
else {
minAsk = minAsk ? __1.BN.max(minAsk, newPrice) : newPrice;
return minAsk;
}
};
while (bidIndex < bids.length || askIndex < asks.length) {
const nextBid = cloneL2Level(bids[bidIndex]);
const nextAsk = cloneL2Level(asks[askIndex]);
if (!nextBid) {
newAsks.push(nextAsk);
askIndex++;
continue;
}
if (!nextAsk) {
newBids.push(nextBid);
bidIndex++;
continue;
}
if (userBids.has(nextBid.price.toString())) {
newBids.push(nextBid);
bidIndex++;
continue;
}
if (userAsks.has(nextAsk.price.toString())) {
newAsks.push(nextAsk);
askIndex++;
continue;
}
if (nextBid.price.gte(nextAsk.price)) {
if (nextBid.price.gt(referencePrice) &&
nextAsk.price.gt(referencePrice)) {
let newBidPrice = nextAsk.price.sub(grouping);
newBidPrice = getPriceAndSetBound(newBidPrice, __1.PositionDirection.LONG);
updateLevels(newBidPrice, nextBid, newBids);
bidIndex++;
}
else if (nextAsk.price.lt(referencePrice) &&
nextBid.price.lt(referencePrice)) {
let newAskPrice = nextBid.price.add(grouping);
newAskPrice = getPriceAndSetBound(newAskPrice, __1.PositionDirection.SHORT);
updateLevels(newAskPrice, nextAsk, newAsks);
askIndex++;
}
else {
let newBidPrice = referencePrice.sub(grouping);
let newAskPrice = referencePrice.add(grouping);
newBidPrice = getPriceAndSetBound(newBidPrice, __1.PositionDirection.LONG);
newAskPrice = getPriceAndSetBound(newAskPrice, __1.PositionDirection.SHORT);
updateLevels(newBidPrice, nextBid, newBids);
updateLevels(newAskPrice, nextAsk, newAsks);
bidIndex++;
askIndex++;
}
}
else {
if (minAsk && nextAsk.price.lte(minAsk)) {
const newAskPrice = getPriceAndSetBound(nextAsk.price, __1.PositionDirection.SHORT);
updateLevels(newAskPrice, nextAsk, newAsks);
}
else {
newAsks.push(nextAsk);
}
askIndex++;
if (maxBid && nextBid.price.gte(maxBid)) {
const newBidPrice = getPriceAndSetBound(nextBid.price, __1.PositionDirection.LONG);
updateLevels(newBidPrice, nextBid, newBids);
}
else {
newBids.push(nextBid);
}
bidIndex++;
}
}
newBids.sort((a, b) => b.price.cmp(a.price));
newAsks.sort((a, b) => a.price.cmp(b.price));
const finalNewBids = mergeByPrice(newBids);
const finalNewAsks = mergeByPrice(newAsks);
return {
bids: finalNewBids,
asks: finalNewAsks,
};
}
exports.uncrossL2 = uncrossL2;