@ox-fun/drift-sdk
Version:
SDK for Drift Protocol
356 lines (305 loc) • 8.07 kB
text/typescript
import { User } from '../user';
import {
isOneOfVariant,
isVariant,
PerpMarketAccount,
AMM,
Order,
PositionDirection,
} from '../types';
import { ZERO, TWO } from '../constants/numericConstants';
import { BN } from '@coral-xyz/anchor';
import { OraclePriceData } from '../oracles/types';
import {
getAuctionPrice,
isAuctionComplete,
isFallbackAvailableLiquiditySource,
} from './auction';
import {
calculateMaxBaseAssetAmountFillable,
calculateMaxBaseAssetAmountToTrade,
calculateUpdatedAMM,
} from './amm';
export function isOrderRiskIncreasing(user: User, order: Order): boolean {
if (isVariant(order.status, 'init')) {
return false;
}
const position =
user.getPerpPosition(order.marketIndex) ||
user.getEmptyPosition(order.marketIndex);
// if no position exists, it's risk increasing
if (position.baseAssetAmount.eq(ZERO)) {
return true;
}
// if position is long and order is long
if (position.baseAssetAmount.gt(ZERO) && isVariant(order.direction, 'long')) {
return true;
}
// if position is short and order is short
if (
position.baseAssetAmount.lt(ZERO) &&
isVariant(order.direction, 'short')
) {
return true;
}
const baseAssetAmountToFill = order.baseAssetAmount.sub(
order.baseAssetAmountFilled
);
// if order will flip position
if (baseAssetAmountToFill.gt(position.baseAssetAmount.abs().mul(TWO))) {
return true;
}
return false;
}
export function isOrderRiskIncreasingInSameDirection(
user: User,
order: Order
): boolean {
if (isVariant(order.status, 'init')) {
return false;
}
const position =
user.getPerpPosition(order.marketIndex) ||
user.getEmptyPosition(order.marketIndex);
// if no position exists, it's risk increasing
if (position.baseAssetAmount.eq(ZERO)) {
return true;
}
// if position is long and order is long
if (position.baseAssetAmount.gt(ZERO) && isVariant(order.direction, 'long')) {
return true;
}
// if position is short and order is short
if (
position.baseAssetAmount.lt(ZERO) &&
isVariant(order.direction, 'short')
) {
return true;
}
return false;
}
export function isOrderReduceOnly(user: User, order: Order): boolean {
if (isVariant(order.status, 'init')) {
return false;
}
const position =
user.getPerpPosition(order.marketIndex) ||
user.getEmptyPosition(order.marketIndex);
// if position is long and order is long
if (
position.baseAssetAmount.gte(ZERO) &&
isVariant(order.direction, 'long')
) {
return false;
}
// if position is short and order is short
if (
position.baseAssetAmount.lte(ZERO) &&
isVariant(order.direction, 'short')
) {
return false;
}
return true;
}
export function standardizeBaseAssetAmount(
baseAssetAmount: BN,
stepSize: BN
): BN {
const remainder = baseAssetAmount.mod(stepSize);
return baseAssetAmount.sub(remainder);
}
export function standardizePrice(
price: BN,
tickSize: BN,
direction: PositionDirection
): BN {
if (price.eq(ZERO)) {
console.log('price is zero');
return price;
}
const remainder = price.mod(tickSize);
if (remainder.eq(ZERO)) {
return price;
}
if (isVariant(direction, 'long')) {
return price.sub(remainder);
} else {
return price.add(tickSize).sub(remainder);
}
}
export function getLimitPrice(
order: Order,
oraclePriceData: OraclePriceData,
slot: number,
fallbackPrice?: BN
): BN | undefined {
let limitPrice;
if (hasAuctionPrice(order, slot)) {
limitPrice = getAuctionPrice(order, slot, oraclePriceData.price);
} else if (order.oraclePriceOffset !== 0) {
limitPrice = oraclePriceData.price.add(new BN(order.oraclePriceOffset));
} else if (order.price.eq(ZERO)) {
limitPrice = fallbackPrice;
} else {
limitPrice = order.price;
}
return limitPrice;
}
export function hasLimitPrice(order: Order, slot: number): boolean {
return (
order.price.gt(ZERO) ||
order.oraclePriceOffset != 0 ||
!isAuctionComplete(order, slot)
);
}
export function hasAuctionPrice(order: Order, slot: number): boolean {
return (
!isAuctionComplete(order, slot) &&
(!order.auctionStartPrice.eq(ZERO) || !order.auctionEndPrice.eq(ZERO))
);
}
export function isFillableByVAMM(
order: Order,
market: PerpMarketAccount,
oraclePriceData: OraclePriceData,
slot: number,
ts: number,
minAuctionDuration: number
): boolean {
return (
(isFallbackAvailableLiquiditySource(order, minAuctionDuration, slot) &&
calculateBaseAssetAmountForAmmToFulfill(
order,
market,
oraclePriceData,
slot
).gte(market.amm.minOrderSize)) ||
isOrderExpired(order, ts)
);
}
export function calculateBaseAssetAmountForAmmToFulfill(
order: Order,
market: PerpMarketAccount,
oraclePriceData: OraclePriceData,
slot: number
): BN {
if (mustBeTriggered(order) && !isTriggered(order)) {
return ZERO;
}
const limitPrice = getLimitPrice(order, oraclePriceData, slot);
let baseAssetAmount;
const updatedAMM = calculateUpdatedAMM(market.amm, oraclePriceData);
if (limitPrice !== undefined) {
baseAssetAmount = calculateBaseAssetAmountToFillUpToLimitPrice(
order,
updatedAMM,
limitPrice,
oraclePriceData
);
} else {
baseAssetAmount = order.baseAssetAmount.sub(order.baseAssetAmountFilled);
}
const maxBaseAssetAmount = calculateMaxBaseAssetAmountFillable(
updatedAMM,
order.direction
);
return BN.min(maxBaseAssetAmount, baseAssetAmount);
}
export function calculateBaseAssetAmountToFillUpToLimitPrice(
order: Order,
amm: AMM,
limitPrice: BN,
oraclePriceData: OraclePriceData
): BN {
const adjustedLimitPrice = isVariant(order.direction, 'long')
? limitPrice.sub(amm.orderTickSize)
: limitPrice.add(amm.orderTickSize);
const [maxAmountToTrade, direction] = calculateMaxBaseAssetAmountToTrade(
amm,
adjustedLimitPrice,
order.direction,
oraclePriceData
);
const baseAssetAmount = standardizeBaseAssetAmount(
maxAmountToTrade,
amm.orderStepSize
);
// Check that directions are the same
const sameDirection = isSameDirection(direction, order.direction);
if (!sameDirection) {
return ZERO;
}
const baseAssetAmountUnfilled = order.baseAssetAmount.sub(
order.baseAssetAmountFilled
);
return baseAssetAmount.gt(baseAssetAmountUnfilled)
? baseAssetAmountUnfilled
: baseAssetAmount;
}
function isSameDirection(
firstDirection: PositionDirection,
secondDirection: PositionDirection
): boolean {
return (
(isVariant(firstDirection, 'long') && isVariant(secondDirection, 'long')) ||
(isVariant(firstDirection, 'short') && isVariant(secondDirection, 'short'))
);
}
export function isOrderExpired(
order: Order,
ts: number,
enforceBuffer = false
): boolean {
if (
mustBeTriggered(order) ||
!isVariant(order.status, 'open') ||
order.maxTs.eq(ZERO)
) {
return false;
}
let maxTs;
if (enforceBuffer && isLimitOrder(order)) {
maxTs = order.maxTs.addn(15);
} else {
maxTs = order.maxTs;
}
return new BN(ts).gt(maxTs);
}
export function isMarketOrder(order: Order): boolean {
return isOneOfVariant(order.orderType, ['market', 'triggerMarket', 'oracle']);
}
export function isLimitOrder(order: Order): boolean {
return isOneOfVariant(order.orderType, ['limit', 'triggerLimit']);
}
export function mustBeTriggered(order: Order): boolean {
return isOneOfVariant(order.orderType, ['triggerMarket', 'triggerLimit']);
}
export function isTriggered(order: Order): boolean {
return isOneOfVariant(order.triggerCondition, [
'triggeredAbove',
'triggeredBelow',
]);
}
export function isRestingLimitOrder(order: Order, slot: number): boolean {
if (!isLimitOrder(order)) {
return false;
}
if (isVariant(order.orderType, 'triggerLimit')) {
if (
isVariant(order.direction, 'long') &&
order.triggerPrice.lt(order.price)
) {
return false;
} else if (
isVariant(order.direction, 'short') &&
order.triggerPrice.gt(order.price)
) {
return false;
}
return isAuctionComplete(order, slot);
}
return order.postOnly || isAuctionComplete(order, slot);
}
export function isTakingOrder(order: Order, slot: number): boolean {
return isMarketOrder(order) || !isRestingLimitOrder(order, slot);
}