@drift-labs/sdk 0.2.0-master.0 → 0.2.0-master.3

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/src/math/amm.ts CHANGED
@@ -6,8 +6,10 @@ import {
6
6
  ZERO,
7
7
  BID_ASK_SPREAD_PRECISION,
8
8
  ONE,
9
- // QUOTE_PRECISION,
10
9
  AMM_TO_QUOTE_PRECISION_RATIO,
10
+ QUOTE_PRECISION,
11
+ MARGIN_PRECISION,
12
+ PRICE_DIV_PEG,
11
13
  } from '../constants/numericConstants';
12
14
  import {
13
15
  AMM,
@@ -25,6 +27,70 @@ import {
25
27
  calculateAdjustKCost,
26
28
  calculateBudgetedPeg,
27
29
  } from './repeg';
30
+
31
+ export function calculatePegFromTargetPrice(
32
+ targetPrice: BN,
33
+ baseAssetReserve: BN,
34
+ quoteAssetReserve: BN
35
+ ): BN {
36
+ return targetPrice
37
+ .mul(baseAssetReserve)
38
+ .div(quoteAssetReserve)
39
+ .add(PRICE_DIV_PEG.div(new BN(2)))
40
+ .div(PRICE_DIV_PEG);
41
+ }
42
+
43
+ export function calculateOptimalPegAndBudget(
44
+ amm: AMM,
45
+ oraclePriceData: OraclePriceData
46
+ ): [BN, BN, BN, boolean] {
47
+ const markPriceBefore = calculatePrice(
48
+ amm.baseAssetReserve,
49
+ amm.quoteAssetReserve,
50
+ amm.pegMultiplier
51
+ );
52
+ const targetPrice = oraclePriceData.price;
53
+ const newPeg = calculatePegFromTargetPrice(
54
+ targetPrice,
55
+ amm.baseAssetReserve,
56
+ amm.quoteAssetReserve
57
+ );
58
+ const prePegCost = calculateRepegCost(amm, newPeg);
59
+
60
+ const totalFeeLB = amm.totalExchangeFee.div(new BN(2));
61
+ const budget = BN.max(ZERO, amm.totalFeeMinusDistributions.sub(totalFeeLB));
62
+ if (budget.lt(prePegCost)) {
63
+ const maxPriceSpread = new BN(amm.maxSpread)
64
+ .mul(targetPrice)
65
+ .div(BID_ASK_SPREAD_PRECISION);
66
+
67
+ let newTargetPrice: BN;
68
+ let newOptimalPeg: BN;
69
+ let newBudget: BN;
70
+ const targetPriceGap = markPriceBefore.sub(targetPrice);
71
+ if (targetPriceGap.abs().gt(maxPriceSpread)) {
72
+ const markAdj = targetPriceGap.abs().sub(maxPriceSpread);
73
+
74
+ if (targetPriceGap.lt(new BN(0))) {
75
+ newTargetPrice = markPriceBefore.add(markAdj);
76
+ } else {
77
+ newTargetPrice = markPriceBefore.sub(markAdj);
78
+ }
79
+
80
+ newOptimalPeg = calculatePegFromTargetPrice(
81
+ newTargetPrice,
82
+ amm.baseAssetReserve,
83
+ amm.quoteAssetReserve
84
+ );
85
+
86
+ newBudget = calculateRepegCost(amm, newOptimalPeg);
87
+ return [newTargetPrice, newOptimalPeg, newBudget, false];
88
+ }
89
+ }
90
+
91
+ return [targetPrice, newPeg, budget, true];
92
+ }
93
+
28
94
  export function calculateNewAmm(
29
95
  amm: AMM,
30
96
  oraclePriceData: OraclePriceData
@@ -32,18 +98,12 @@ export function calculateNewAmm(
32
98
  let pKNumer = new BN(1);
33
99
  let pKDenom = new BN(1);
34
100
 
35
- const targetPrice = oraclePriceData.price;
36
- let newPeg = targetPrice
37
- .mul(amm.baseAssetReserve)
38
- .div(amm.quoteAssetReserve)
39
- .add(MARK_PRICE_PRECISION.div(PEG_PRECISION).div(new BN(2)))
40
- .div(MARK_PRICE_PRECISION.div(PEG_PRECISION));
41
- let prePegCost = calculateRepegCost(amm, newPeg);
42
-
43
- const totalFeeLB = amm.totalExchangeFee.div(new BN(2));
44
- const budget = BN.max(ZERO, amm.totalFeeMinusDistributions.sub(totalFeeLB));
101
+ const [targetPrice, _newPeg, budget, checkLowerBound] =
102
+ calculateOptimalPegAndBudget(amm, oraclePriceData);
103
+ let prePegCost = calculateRepegCost(amm, _newPeg);
104
+ let newPeg = _newPeg;
45
105
 
46
- if (prePegCost.gt(budget)) {
106
+ if (prePegCost.gt(budget) && checkLowerBound) {
47
107
  [pKNumer, pKDenom] = [new BN(999), new BN(1000)];
48
108
  const deficitMadeup = calculateAdjustKCost(amm, pKNumer, pKDenom);
49
109
  assert(deficitMadeup.lte(new BN(0)));
@@ -136,9 +196,16 @@ export function calculateUpdatedAMMSpreadReserves(
136
196
 
137
197
  export function calculateBidAskPrice(
138
198
  amm: AMM,
139
- oraclePriceData: OraclePriceData
199
+ oraclePriceData: OraclePriceData,
200
+ withUpdate = true
140
201
  ): [BN, BN] {
141
- const newAmm = calculateUpdatedAMM(amm, oraclePriceData);
202
+ let newAmm: AMM;
203
+ if (withUpdate) {
204
+ newAmm = calculateUpdatedAMM(amm, oraclePriceData);
205
+ } else {
206
+ newAmm = amm;
207
+ }
208
+
142
209
  const askReserves = calculateSpreadReserves(
143
210
  newAmm,
144
211
  PositionDirection.LONG,
@@ -237,6 +304,108 @@ export function calculateAmmReservesAfterSwap(
237
304
  return [newQuoteAssetReserve, newBaseAssetReserve];
238
305
  }
239
306
 
307
+ export function calculateEffectiveLeverage(
308
+ baseSpread: number,
309
+ quoteAssetReserve: BN,
310
+ terminalQuoteAssetReserve: BN,
311
+ pegMultiplier: BN,
312
+ netBaseAssetAmount: BN,
313
+ markPrice: BN,
314
+ totalFeeMinusDistributions: BN
315
+ ): number {
316
+ // inventory skew
317
+ const netBaseAssetValue = quoteAssetReserve
318
+ .sub(terminalQuoteAssetReserve)
319
+ .mul(pegMultiplier)
320
+ .div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO);
321
+
322
+ const localBaseAssetValue = netBaseAssetAmount
323
+ .mul(markPrice)
324
+ .div(AMM_TO_QUOTE_PRECISION_RATIO.mul(MARK_PRICE_PRECISION));
325
+
326
+ const effectiveLeverage =
327
+ localBaseAssetValue.sub(netBaseAssetValue).toNumber() /
328
+ (Math.max(0, totalFeeMinusDistributions.toNumber()) + 1) +
329
+ 1 / QUOTE_PRECISION.toNumber();
330
+
331
+ return effectiveLeverage;
332
+ }
333
+
334
+ export function calculateMaxSpread(marginRatioInitial: number): number {
335
+ const maxTargetSpread: number = new BN(marginRatioInitial)
336
+ .mul(BID_ASK_SPREAD_PRECISION.div(MARGIN_PRECISION))
337
+ .toNumber();
338
+
339
+ return maxTargetSpread;
340
+ }
341
+
342
+ export function calculateSpreadBN(
343
+ baseSpread: number,
344
+ lastOracleMarkSpreadPct: BN,
345
+ lastOracleConfPct: BN,
346
+ maxSpread: number,
347
+ quoteAssetReserve: BN,
348
+ terminalQuoteAssetReserve: BN,
349
+ pegMultiplier: BN,
350
+ netBaseAssetAmount: BN,
351
+ markPrice: BN,
352
+ totalFeeMinusDistributions: BN
353
+ ): [number, number] {
354
+ let longSpread = baseSpread / 2;
355
+ let shortSpread = baseSpread / 2;
356
+
357
+ if (lastOracleMarkSpreadPct.gt(ZERO)) {
358
+ shortSpread = Math.max(
359
+ shortSpread,
360
+ lastOracleMarkSpreadPct.abs().toNumber() + lastOracleConfPct.toNumber()
361
+ );
362
+ } else if (lastOracleMarkSpreadPct.lt(ZERO)) {
363
+ longSpread = Math.max(
364
+ longSpread,
365
+ lastOracleMarkSpreadPct.abs().toNumber() + lastOracleConfPct.toNumber()
366
+ );
367
+ }
368
+
369
+ const maxTargetSpread: number = maxSpread;
370
+
371
+ const MAX_INVENTORY_SKEW = 5;
372
+
373
+ const effectiveLeverage = calculateEffectiveLeverage(
374
+ baseSpread,
375
+ quoteAssetReserve,
376
+ terminalQuoteAssetReserve,
377
+ pegMultiplier,
378
+ netBaseAssetAmount,
379
+ markPrice,
380
+ totalFeeMinusDistributions
381
+ );
382
+
383
+ if (totalFeeMinusDistributions.gt(ZERO)) {
384
+ const spreadScale = Math.min(MAX_INVENTORY_SKEW, 1 + effectiveLeverage);
385
+ if (netBaseAssetAmount.gt(ZERO)) {
386
+ longSpread *= spreadScale;
387
+ } else {
388
+ shortSpread *= spreadScale;
389
+ }
390
+ } else {
391
+ longSpread *= MAX_INVENTORY_SKEW;
392
+ shortSpread *= MAX_INVENTORY_SKEW;
393
+ }
394
+
395
+ const totalSpread = longSpread + shortSpread;
396
+ if (totalSpread > maxTargetSpread) {
397
+ if (longSpread > shortSpread) {
398
+ longSpread = Math.min(longSpread, maxTargetSpread);
399
+ shortSpread = maxTargetSpread - longSpread;
400
+ } else {
401
+ shortSpread = Math.min(shortSpread, maxTargetSpread);
402
+ longSpread = maxTargetSpread - shortSpread;
403
+ }
404
+ }
405
+
406
+ return [longSpread, shortSpread];
407
+ }
408
+
240
409
  export function calculateSpread(
241
410
  amm: AMM,
242
411
  direction: PositionDirection,
@@ -255,18 +424,26 @@ export function calculateSpread(
255
424
  );
256
425
 
257
426
  const targetPrice = oraclePriceData?.price || markPrice;
427
+ const confInterval = oraclePriceData.confidence || ZERO;
258
428
 
259
429
  const targetMarkSpreadPct = markPrice
260
430
  .sub(targetPrice)
261
431
  .mul(BID_ASK_SPREAD_PRECISION)
262
432
  .div(markPrice);
263
433
 
434
+ const confIntervalPct = confInterval
435
+ .mul(BID_ASK_SPREAD_PRECISION)
436
+ .div(markPrice);
437
+
264
438
  // oracle retreat
265
439
  if (
266
440
  (isVariant(direction, 'long') && targetMarkSpreadPct.lt(ZERO)) ||
267
441
  (isVariant(direction, 'short') && targetMarkSpreadPct.gt(ZERO))
268
442
  ) {
269
- spread = Math.max(spread, targetMarkSpreadPct.abs().toNumber());
443
+ spread = Math.max(
444
+ spread,
445
+ targetMarkSpreadPct.abs().toNumber() + confIntervalPct.abs().toNumber()
446
+ );
270
447
  }
271
448
 
272
449
  // inventory skew
@@ -294,10 +471,18 @@ export function calculateSpread(
294
471
  if (amm.totalFeeMinusDistributions.gt(ZERO)) {
295
472
  effectiveLeverage =
296
473
  localPnl.sub(netPnl).toNumber() /
297
- amm.totalFeeMinusDistributions.toNumber();
474
+ (amm.totalFeeMinusDistributions.toNumber() + 1);
475
+ }
476
+
477
+ let spreadScale = Math.min(MAX_INVENTORY_SKEW, 1 + effectiveLeverage);
478
+ const maxTargetSpread = BID_ASK_SPREAD_PRECISION.toNumber() / 50; // 2%
479
+ // cap the scale to attempt to only scale up to maxTargetSpread
480
+ // always let the oracle retreat methods go through 100%
481
+ if (spreadScale * spread > maxTargetSpread) {
482
+ spreadScale = Math.max(1.05, maxTargetSpread / spread);
298
483
  }
299
484
 
300
- spread *= Math.min(MAX_INVENTORY_SKEW, 1 + effectiveLeverage);
485
+ spread *= spreadScale;
301
486
  }
302
487
 
303
488
  return spread;
@@ -1,5 +1,5 @@
1
1
  import { ClearingHouseUser } from '../clearingHouseUser';
2
- import { isVariant, Order } from '../types';
2
+ import { isOneOfVariant, isVariant, Order } from '../types';
3
3
  import { ZERO, TWO } from '../constants/numericConstants';
4
4
  import { BN } from '@project-serum/anchor';
5
5
  import { OraclePriceData } from '../oracles/types';
@@ -125,18 +125,8 @@ export function getLimitPrice(
125
125
  ): BN {
126
126
  let limitPrice;
127
127
  if (!order.oraclePriceOffset.eq(ZERO)) {
128
- const floatingPrice = oraclePriceData.price.add(order.oraclePriceOffset);
129
- if (order.postOnly) {
130
- limitPrice = isVariant(order.direction, 'long')
131
- ? BN.min(order.price, floatingPrice)
132
- : BN.max(order.price, floatingPrice);
133
- } else {
134
- limitPrice = floatingPrice;
135
- }
136
- } else if (
137
- isVariant(order.orderType, 'market') ||
138
- isVariant(order.orderType, 'triggerMarket')
139
- ) {
128
+ limitPrice = oraclePriceData.price.add(order.oraclePriceOffset);
129
+ } else if (isOneOfVariant(order.orderType, ['market', 'triggerMarket'])) {
140
130
  limitPrice = getAuctionPrice(order, slot);
141
131
  } else {
142
132
  limitPrice = order.price;
package/src/math/repeg.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  AMM_RESERVE_PRECISION,
6
6
  PEG_PRECISION,
7
7
  AMM_TO_QUOTE_PRECISION_RATIO,
8
+ PRICE_DIV_PEG,
8
9
  QUOTE_PRECISION,
9
10
  ZERO,
10
11
  } from '../constants/numericConstants';
@@ -142,7 +143,7 @@ export function calculateBudgetedPeg(amm: AMM, cost: BN, targetPrice: BN): BN {
142
143
  const targetPeg = targetPrice
143
144
  .mul(amm.baseAssetReserve)
144
145
  .div(amm.quoteAssetReserve)
145
- .div(MARK_PRICE_PRECISION.div(PEG_PRECISION));
146
+ .div(PRICE_DIV_PEG);
146
147
 
147
148
  const k = amm.sqrtK.mul(amm.sqrtK);
148
149
  const x = amm.baseAssetReserve;
package/src/orders.ts CHANGED
@@ -3,27 +3,13 @@ import {
3
3
  MarketAccount,
4
4
  Order,
5
5
  PositionDirection,
6
- SwapDirection,
7
6
  UserAccount,
8
7
  UserPosition,
9
8
  } from './types';
10
- import {
11
- BN,
12
- calculateAmmReservesAfterSwap,
13
- calculateBaseAssetValue,
14
- calculateSpreadReserves,
15
- ClearingHouseUser,
16
- isOrderRiskIncreasingInSameDirection,
17
- standardizeBaseAssetAmount,
18
- TEN_THOUSAND,
19
- } from '.';
20
- import {
21
- calculateMarkPrice,
22
- calculateNewMarketAfterTrade,
23
- } from './math/market';
9
+ import { BN, standardizeBaseAssetAmount } from '.';
10
+ import { calculateNewMarketAfterTrade } from './math/market';
24
11
  import {
25
12
  AMM_TO_QUOTE_PRECISION_RATIO,
26
- TWO,
27
13
  PEG_PRECISION,
28
14
  ZERO,
29
15
  } from './constants/numericConstants';
@@ -182,7 +168,6 @@ export function calculateBaseAssetAmountMarketCanExecute(
182
168
  } else if (isVariant(order.orderType, 'triggerLimit')) {
183
169
  return calculateAmountToTradeForTriggerLimit(market, order);
184
170
  } else if (isVariant(order.orderType, 'market')) {
185
- // should never be a market order queued
186
171
  return ZERO;
187
172
  } else {
188
173
  return calculateAmountToTradeForTriggerMarket(market, order);
@@ -201,14 +186,7 @@ export function calculateAmountToTradeForLimit(
201
186
  'Cant calculate limit price for oracle offset oracle without OraclePriceData'
202
187
  );
203
188
  }
204
- const floatingPrice = oraclePriceData.price.add(order.oraclePriceOffset);
205
- if (order.postOnly) {
206
- limitPrice = isVariant(order.direction, 'long')
207
- ? BN.min(order.price, floatingPrice)
208
- : BN.max(order.price, floatingPrice);
209
- } else {
210
- limitPrice = floatingPrice;
211
- }
189
+ limitPrice = oraclePriceData.price.add(order.oraclePriceOffset);
212
190
  }
213
191
 
214
192
  const [maxAmountToTrade, direction] = calculateMaxBaseAssetAmountToTrade(
@@ -237,14 +215,8 @@ export function calculateAmountToTradeForTriggerLimit(
237
215
  market: MarketAccount,
238
216
  order: Order
239
217
  ): BN {
240
- if (order.baseAssetAmountFilled.eq(ZERO)) {
241
- const baseAssetAmount = calculateAmountToTradeForTriggerMarket(
242
- market,
243
- order
244
- );
245
- if (baseAssetAmount.eq(ZERO)) {
246
- return ZERO;
247
- }
218
+ if (!order.triggered) {
219
+ return ZERO;
248
220
  }
249
221
 
250
222
  return calculateAmountToTradeForLimit(market, order);
@@ -264,105 +236,9 @@ function calculateAmountToTradeForTriggerMarket(
264
236
  market: MarketAccount,
265
237
  order: Order
266
238
  ): BN {
267
- return isTriggerConditionSatisfied(market, order)
268
- ? order.baseAssetAmount
269
- : ZERO;
270
- }
271
-
272
- function isTriggerConditionSatisfied(
273
- market: MarketAccount,
274
- order: Order,
275
- oraclePriceData?: OraclePriceData
276
- ): boolean {
277
- const markPrice = calculateMarkPrice(market, oraclePriceData);
278
- if (isVariant(order.triggerCondition, 'above')) {
279
- return markPrice.gt(order.triggerPrice);
280
- } else {
281
- return markPrice.lt(order.triggerPrice);
282
- }
283
- }
284
-
285
- export function calculateBaseAssetAmountUserCanExecute(
286
- market: MarketAccount,
287
- order: Order,
288
- user: ClearingHouseUser,
289
- oraclePriceData?: OraclePriceData
290
- ): BN {
291
- const maxLeverage = user.getMaxLeverage(order.marketIndex, 'Initial');
292
- const freeCollateral = user.getFreeCollateral();
293
- let quoteAssetAmount: BN;
294
- if (isOrderRiskIncreasingInSameDirection(user, order)) {
295
- quoteAssetAmount = freeCollateral.mul(maxLeverage).div(TEN_THOUSAND);
296
- } else {
297
- const position =
298
- user.getUserPosition(order.marketIndex) ||
299
- user.getEmptyPosition(order.marketIndex);
300
- const positionValue = calculateBaseAssetValue(
301
- market,
302
- position,
303
- oraclePriceData
304
- );
305
- quoteAssetAmount = freeCollateral
306
- .mul(maxLeverage)
307
- .div(TEN_THOUSAND)
308
- .add(positionValue.mul(TWO));
309
- }
310
-
311
- if (quoteAssetAmount.lte(ZERO)) {
239
+ if (!order.triggered) {
312
240
  return ZERO;
313
241
  }
314
242
 
315
- const swapDirection = isVariant(order.direction, 'long')
316
- ? SwapDirection.ADD
317
- : SwapDirection.REMOVE;
318
-
319
- const useSpread = !order.postOnly;
320
- let amm: Parameters<typeof calculateAmmReservesAfterSwap>[0];
321
- if (useSpread) {
322
- const { baseAssetReserve, quoteAssetReserve } = calculateSpreadReserves(
323
- market.amm,
324
- order.direction,
325
- oraclePriceData
326
- );
327
- amm = {
328
- baseAssetReserve,
329
- quoteAssetReserve,
330
- sqrtK: market.amm.sqrtK,
331
- pegMultiplier: market.amm.pegMultiplier,
332
- };
333
- } else {
334
- amm = market.amm;
335
- }
336
-
337
- const baseAssetReservesBefore = amm.baseAssetReserve;
338
- const [_, baseAssetReservesAfter] = calculateAmmReservesAfterSwap(
339
- amm,
340
- 'quote',
341
- quoteAssetAmount,
342
- swapDirection
343
- );
344
-
345
- let baseAssetAmount = baseAssetReservesBefore
346
- .sub(baseAssetReservesAfter)
347
- .abs();
348
- if (order.reduceOnly) {
349
- const position =
350
- user.getUserPosition(order.marketIndex) ||
351
- user.getEmptyPosition(order.marketIndex);
352
- if (
353
- isVariant(order.direction, 'long') &&
354
- position.baseAssetAmount.gte(ZERO)
355
- ) {
356
- baseAssetAmount = ZERO;
357
- } else if (
358
- isVariant(order.direction, 'short') &&
359
- position.baseAssetAmount.lte(ZERO)
360
- ) {
361
- baseAssetAmount = ZERO;
362
- } else {
363
- BN.min(baseAssetAmount, position.baseAssetAmount.abs());
364
- }
365
- }
366
-
367
- return baseAssetAmount;
243
+ return order.baseAssetAmount;
368
244
  }
package/src/types.ts CHANGED
@@ -48,6 +48,7 @@ export class OrderAction {
48
48
  static readonly CANCEL = { cancel: {} };
49
49
  static readonly EXPIRE = { expire: {} };
50
50
  static readonly FILL = { fill: {} };
51
+ static readonly TRIGGER = { trigger: {} };
51
52
  }
52
53
 
53
54
  export class OrderTriggerCondition {
@@ -290,6 +291,7 @@ export type AMM = {
290
291
  lastAskPriceTwap: BN;
291
292
  longSpread: BN;
292
293
  shortSpread: BN;
294
+ maxSpread: number;
293
295
  };
294
296
 
295
297
  // # User Account Types
@@ -347,8 +349,9 @@ export type Order = {
347
349
  reduceOnly: boolean;
348
350
  triggerPrice: BN;
349
351
  triggerCondition: OrderTriggerCondition;
352
+ triggered: boolean;
350
353
  discountTier: OrderDiscountTier;
351
- existingPositionDirection: PositionDirection,
354
+ existingPositionDirection: PositionDirection;
352
355
  referrer: PublicKey;
353
356
  postOnly: boolean;
354
357
  immediateOrCancel: boolean;