@drift-labs/sdk 2.12.0-beta.3 → 2.13.0-beta.0
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/lib/dlob/DLOB.d.ts +8 -8
- package/lib/dlob/DLOB.js +24 -17
- package/lib/examples/loadDlob.d.ts +1 -0
- package/lib/examples/loadDlob.js +54 -0
- package/lib/idl/drift.json +1 -1
- package/lib/math/trade.d.ts +35 -3
- package/lib/math/trade.js +192 -14
- package/package.json +1 -1
- package/src/dlob/DLOB.ts +34 -26
- package/src/examples/loadDlob.ts +82 -0
- package/src/idl/drift.json +1 -1
- package/src/math/trade.ts +322 -16
- package/tests/dlob/test.ts +307 -38
- package/src/examples/makeTradeExample.js +0 -157
package/src/dlob/DLOB.ts
CHANGED
|
@@ -519,10 +519,11 @@ export class DLOB {
|
|
|
519
519
|
marketType,
|
|
520
520
|
oraclePriceData,
|
|
521
521
|
marketOrderGenerator,
|
|
522
|
-
this.
|
|
522
|
+
this.getMakerLimitBids.bind(this),
|
|
523
523
|
(takerPrice, makerPrice) => {
|
|
524
524
|
return takerPrice === undefined || takerPrice.lte(makerPrice);
|
|
525
|
-
}
|
|
525
|
+
},
|
|
526
|
+
fallbackAsk
|
|
526
527
|
);
|
|
527
528
|
for (const marketAskCrossingBid of marketAsksCrossingBids) {
|
|
528
529
|
nodesToFill.push(marketAskCrossingBid);
|
|
@@ -555,10 +556,11 @@ export class DLOB {
|
|
|
555
556
|
marketType,
|
|
556
557
|
oraclePriceData,
|
|
557
558
|
marketOrderGenerator,
|
|
558
|
-
this.
|
|
559
|
+
this.getMakerLimitAsks.bind(this),
|
|
559
560
|
(takerPrice, fallbackPrice) => {
|
|
560
561
|
return takerPrice === undefined || takerPrice.gte(fallbackPrice);
|
|
561
|
-
}
|
|
562
|
+
},
|
|
563
|
+
fallbackBid
|
|
562
564
|
);
|
|
563
565
|
|
|
564
566
|
for (const marketBidToFill of marketBidsToFill) {
|
|
@@ -596,9 +598,11 @@ export class DLOB {
|
|
|
596
598
|
marketIndex: number,
|
|
597
599
|
slot: number,
|
|
598
600
|
marketType: MarketType,
|
|
599
|
-
oraclePriceData: OraclePriceData
|
|
601
|
+
oraclePriceData: OraclePriceData,
|
|
602
|
+
fallbackPrice?: BN
|
|
600
603
|
) => Generator<DLOBNode>,
|
|
601
|
-
doesCross: (takerPrice: BN | undefined, makerPrice: BN) => boolean
|
|
604
|
+
doesCross: (takerPrice: BN | undefined, makerPrice: BN) => boolean,
|
|
605
|
+
fallbackPrice?: BN
|
|
602
606
|
): NodeToFill[] {
|
|
603
607
|
const nodesToFill = new Array<NodeToFill>();
|
|
604
608
|
|
|
@@ -607,7 +611,8 @@ export class DLOB {
|
|
|
607
611
|
marketIndex,
|
|
608
612
|
slot,
|
|
609
613
|
marketType,
|
|
610
|
-
oraclePriceData
|
|
614
|
+
oraclePriceData,
|
|
615
|
+
fallbackPrice
|
|
611
616
|
);
|
|
612
617
|
|
|
613
618
|
for (const makerNode of makerNodeGenerator) {
|
|
@@ -916,17 +921,17 @@ export class DLOB {
|
|
|
916
921
|
}
|
|
917
922
|
|
|
918
923
|
/**
|
|
919
|
-
* Filters the limit asks that are post only
|
|
920
|
-
*
|
|
924
|
+
* Filters the limit asks that are post only, have been place for sufficiently long or are above the fallback bid
|
|
925
|
+
* Market orders can only fill against orders that meet this criteria
|
|
921
926
|
*
|
|
922
927
|
* @returns
|
|
923
928
|
*/
|
|
924
|
-
*
|
|
929
|
+
*getMakerLimitAsks(
|
|
925
930
|
marketIndex: number,
|
|
926
931
|
slot: number,
|
|
927
932
|
marketType: MarketType,
|
|
928
933
|
oraclePriceData: OraclePriceData,
|
|
929
|
-
|
|
934
|
+
fallbackBid?: BN
|
|
930
935
|
): Generator<DLOBNode> {
|
|
931
936
|
for (const node of this.getLimitAsks(
|
|
932
937
|
marketIndex,
|
|
@@ -934,21 +939,19 @@ export class DLOB {
|
|
|
934
939
|
marketType,
|
|
935
940
|
oraclePriceData
|
|
936
941
|
)) {
|
|
937
|
-
if (this.isRestingLimitOrder(node.order, slot
|
|
942
|
+
if (this.isRestingLimitOrder(node.order, slot)) {
|
|
943
|
+
yield node;
|
|
944
|
+
} else if (
|
|
945
|
+
fallbackBid &&
|
|
946
|
+
node.getPrice(oraclePriceData, slot).gt(fallbackBid)
|
|
947
|
+
) {
|
|
938
948
|
yield node;
|
|
939
949
|
}
|
|
940
950
|
}
|
|
941
951
|
}
|
|
942
952
|
|
|
943
|
-
isRestingLimitOrder(
|
|
944
|
-
order
|
|
945
|
-
slot: number,
|
|
946
|
-
minPerpAuctionDuration: number
|
|
947
|
-
): boolean {
|
|
948
|
-
return (
|
|
949
|
-
order.postOnly ||
|
|
950
|
-
new BN(slot).sub(order.slot).gte(new BN(minPerpAuctionDuration * 1.5))
|
|
951
|
-
);
|
|
953
|
+
isRestingLimitOrder(order: Order, slot: number): boolean {
|
|
954
|
+
return order.postOnly || new BN(slot).sub(order.slot).gte(new BN(15));
|
|
952
955
|
}
|
|
953
956
|
|
|
954
957
|
*getLimitBids(
|
|
@@ -984,17 +987,17 @@ export class DLOB {
|
|
|
984
987
|
}
|
|
985
988
|
|
|
986
989
|
/**
|
|
987
|
-
* Filters the limit bids that are post only
|
|
988
|
-
*
|
|
990
|
+
* Filters the limit bids that are post only, have been place for sufficiently long or are below the fallback ask
|
|
991
|
+
* Market orders can only fill against orders that meet this criteria
|
|
989
992
|
*
|
|
990
993
|
* @returns
|
|
991
994
|
*/
|
|
992
|
-
*
|
|
995
|
+
*getMakerLimitBids(
|
|
993
996
|
marketIndex: number,
|
|
994
997
|
slot: number,
|
|
995
998
|
marketType: MarketType,
|
|
996
999
|
oraclePriceData: OraclePriceData,
|
|
997
|
-
|
|
1000
|
+
fallbackAsk?: BN
|
|
998
1001
|
): Generator<DLOBNode> {
|
|
999
1002
|
for (const node of this.getLimitBids(
|
|
1000
1003
|
marketIndex,
|
|
@@ -1002,7 +1005,12 @@ export class DLOB {
|
|
|
1002
1005
|
marketType,
|
|
1003
1006
|
oraclePriceData
|
|
1004
1007
|
)) {
|
|
1005
|
-
if (this.isRestingLimitOrder(node.order, slot
|
|
1008
|
+
if (this.isRestingLimitOrder(node.order, slot)) {
|
|
1009
|
+
yield node;
|
|
1010
|
+
} else if (
|
|
1011
|
+
fallbackAsk &&
|
|
1012
|
+
node.getPrice(oraclePriceData, slot).lt(fallbackAsk)
|
|
1013
|
+
) {
|
|
1006
1014
|
yield node;
|
|
1007
1015
|
}
|
|
1008
1016
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AnchorProvider } from '@project-serum/anchor';
|
|
2
|
+
import { DLOB, UserMap, Wallet } from '..';
|
|
3
|
+
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
4
|
+
import {
|
|
5
|
+
DriftClient,
|
|
6
|
+
initialize,
|
|
7
|
+
BulkAccountLoader,
|
|
8
|
+
getMarketsAndOraclesForSubscription,
|
|
9
|
+
} from '..';
|
|
10
|
+
|
|
11
|
+
const env = 'mainnet-beta';
|
|
12
|
+
|
|
13
|
+
const main = async () => {
|
|
14
|
+
// Initialize Drift SDK
|
|
15
|
+
const sdkConfig = initialize({ env });
|
|
16
|
+
|
|
17
|
+
// Set up the Wallet and Provider
|
|
18
|
+
const privateKey = process.env.BOT_PRIVATE_KEY; // stored as an array string
|
|
19
|
+
const keypair = Keypair.fromSecretKey(
|
|
20
|
+
Uint8Array.from(JSON.parse(privateKey))
|
|
21
|
+
);
|
|
22
|
+
const wallet = new Wallet(keypair);
|
|
23
|
+
|
|
24
|
+
// Set up the Connection
|
|
25
|
+
const rpcAddress = process.env.RPC_ADDRESS; // can use: https://api.devnet.solana.com for devnet; https://api.mainnet-beta.solana.com for mainnet;
|
|
26
|
+
const connection = new Connection(rpcAddress);
|
|
27
|
+
|
|
28
|
+
// Set up the Provider
|
|
29
|
+
const provider = new AnchorProvider(
|
|
30
|
+
connection,
|
|
31
|
+
wallet,
|
|
32
|
+
AnchorProvider.defaultOptions()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Set up the Drift Clearing House
|
|
36
|
+
const driftPublicKey = new PublicKey(sdkConfig.DRIFT_PROGRAM_ID);
|
|
37
|
+
const bulkAccountLoader = new BulkAccountLoader(
|
|
38
|
+
connection,
|
|
39
|
+
'confirmed',
|
|
40
|
+
1000
|
|
41
|
+
);
|
|
42
|
+
const driftClient = new DriftClient({
|
|
43
|
+
connection,
|
|
44
|
+
wallet: provider.wallet,
|
|
45
|
+
programID: driftPublicKey,
|
|
46
|
+
...getMarketsAndOraclesForSubscription(env),
|
|
47
|
+
accountSubscription: {
|
|
48
|
+
type: 'polling',
|
|
49
|
+
accountLoader: bulkAccountLoader,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log('Subscribing drift client...');
|
|
54
|
+
await driftClient.subscribe();
|
|
55
|
+
|
|
56
|
+
console.log('Loading user map...');
|
|
57
|
+
const userMap = new UserMap(driftClient, {
|
|
58
|
+
type: 'polling',
|
|
59
|
+
accountLoader: bulkAccountLoader,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// fetches all users and subscribes for updates
|
|
63
|
+
await userMap.fetchAllUsers();
|
|
64
|
+
|
|
65
|
+
console.log('Loading dlob from user map...');
|
|
66
|
+
const dlob = new DLOB();
|
|
67
|
+
await dlob.initFromUserMap(userMap);
|
|
68
|
+
|
|
69
|
+
console.log('number of orders', dlob.getDLOBOrders().length);
|
|
70
|
+
|
|
71
|
+
dlob.clear();
|
|
72
|
+
|
|
73
|
+
console.log('Unsubscribing users...');
|
|
74
|
+
for (const user of userMap.values()) {
|
|
75
|
+
await user.unsubscribe();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('Unsubscribing drift client...');
|
|
79
|
+
await driftClient.unsubscribe();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
main();
|
package/src/idl/drift.json
CHANGED
package/src/math/trade.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
MarketType,
|
|
3
|
+
PerpMarketAccount,
|
|
4
|
+
PositionDirection,
|
|
5
|
+
SpotMarketAccount,
|
|
6
|
+
} from '../types';
|
|
2
7
|
import { BN } from '@project-serum/anchor';
|
|
3
8
|
import { assert } from '../assert/assert';
|
|
4
9
|
import {
|
|
@@ -7,6 +12,7 @@ import {
|
|
|
7
12
|
AMM_TO_QUOTE_PRECISION_RATIO,
|
|
8
13
|
ZERO,
|
|
9
14
|
BASE_PRECISION,
|
|
15
|
+
BN_MAX,
|
|
10
16
|
} from '../constants/numericConstants';
|
|
11
17
|
import {
|
|
12
18
|
calculateBidPrice,
|
|
@@ -20,11 +26,14 @@ import {
|
|
|
20
26
|
AssetType,
|
|
21
27
|
calculateUpdatedAMMSpreadReserves,
|
|
22
28
|
calculateQuoteAssetAmountSwapped,
|
|
29
|
+
calculateMarketOpenBidAsk,
|
|
23
30
|
} from './amm';
|
|
24
31
|
import { squareRootBN } from './utils';
|
|
25
32
|
import { isVariant } from '../types';
|
|
26
33
|
import { OraclePriceData } from '../oracles/types';
|
|
27
34
|
import { DLOB } from '../dlob/DLOB';
|
|
35
|
+
import { PublicKey } from '@solana/web3.js';
|
|
36
|
+
import { Orderbook } from '@project-serum/serum';
|
|
28
37
|
|
|
29
38
|
const MAXPCT = new BN(1000); //percentage units are [0,1000] => [0,1]
|
|
30
39
|
|
|
@@ -363,7 +372,7 @@ export function calculateTargetPriceTrade(
|
|
|
363
372
|
* @param oraclePriceData
|
|
364
373
|
* @param dlob
|
|
365
374
|
* @param slot
|
|
366
|
-
* @param
|
|
375
|
+
* @param usersToSkip
|
|
367
376
|
*/
|
|
368
377
|
export function calculateEstimatedPerpEntryPrice(
|
|
369
378
|
assetType: AssetType,
|
|
@@ -373,21 +382,37 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
373
382
|
oraclePriceData: OraclePriceData,
|
|
374
383
|
dlob: DLOB,
|
|
375
384
|
slot: number,
|
|
376
|
-
|
|
377
|
-
):
|
|
385
|
+
usersToSkip = new Map<PublicKey, boolean>()
|
|
386
|
+
): {
|
|
387
|
+
entryPrice: BN;
|
|
388
|
+
priceImpact: BN;
|
|
389
|
+
bestPrice: BN;
|
|
390
|
+
worstPrice: BN;
|
|
391
|
+
baseFilled: BN;
|
|
392
|
+
quoteFilled: BN;
|
|
393
|
+
} {
|
|
378
394
|
if (amount.eq(ZERO)) {
|
|
379
|
-
return
|
|
395
|
+
return {
|
|
396
|
+
entryPrice: ZERO,
|
|
397
|
+
priceImpact: ZERO,
|
|
398
|
+
bestPrice: ZERO,
|
|
399
|
+
worstPrice: ZERO,
|
|
400
|
+
baseFilled: ZERO,
|
|
401
|
+
quoteFilled: ZERO,
|
|
402
|
+
};
|
|
380
403
|
}
|
|
381
404
|
|
|
382
405
|
const takerIsLong = isVariant(direction, 'long');
|
|
383
406
|
const limitOrders = dlob[
|
|
384
|
-
takerIsLong ? '
|
|
407
|
+
takerIsLong ? 'getMakerLimitAsks' : 'getMakerLimitBids'
|
|
385
408
|
](
|
|
386
409
|
market.marketIndex,
|
|
387
410
|
slot,
|
|
388
411
|
MarketType.PERP,
|
|
389
412
|
oraclePriceData,
|
|
390
|
-
|
|
413
|
+
takerIsLong
|
|
414
|
+
? calculateBidPrice(market, oraclePriceData)
|
|
415
|
+
: calculateAskPrice(market, oraclePriceData)
|
|
391
416
|
);
|
|
392
417
|
|
|
393
418
|
const swapDirection = getSwapDirection(assetType, direction);
|
|
@@ -401,9 +426,34 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
401
426
|
pegMultiplier: newPeg,
|
|
402
427
|
};
|
|
403
428
|
|
|
429
|
+
const [ammBids, ammAsks] = calculateMarketOpenBidAsk(
|
|
430
|
+
market.amm.baseAssetReserve,
|
|
431
|
+
market.amm.minBaseAssetReserve,
|
|
432
|
+
market.amm.maxBaseAssetReserve,
|
|
433
|
+
market.amm.orderStepSize
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
let ammLiquidity: BN;
|
|
437
|
+
if (assetType === 'base') {
|
|
438
|
+
ammLiquidity = takerIsLong ? ammAsks.abs() : ammBids;
|
|
439
|
+
} else {
|
|
440
|
+
const [afterSwapQuoteReserves, _] = calculateAmmReservesAfterSwap(
|
|
441
|
+
amm,
|
|
442
|
+
'base',
|
|
443
|
+
takerIsLong ? ammAsks.abs() : ammBids,
|
|
444
|
+
getSwapDirection('base', direction)
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
ammLiquidity = calculateQuoteAssetAmountSwapped(
|
|
448
|
+
amm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(),
|
|
449
|
+
amm.pegMultiplier,
|
|
450
|
+
swapDirection
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
404
454
|
const invariant = amm.sqrtK.mul(amm.sqrtK);
|
|
405
455
|
|
|
406
|
-
let
|
|
456
|
+
let bestPrice = calculatePrice(
|
|
407
457
|
amm.baseAssetReserve,
|
|
408
458
|
amm.quoteAssetReserve,
|
|
409
459
|
amm.pegMultiplier
|
|
@@ -415,13 +465,18 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
415
465
|
let limitOrder = limitOrders.next().value;
|
|
416
466
|
if (limitOrder) {
|
|
417
467
|
const limitOrderPrice = limitOrder.getPrice(oraclePriceData, slot);
|
|
418
|
-
|
|
419
|
-
? BN.min(limitOrderPrice,
|
|
420
|
-
: BN.max(limitOrderPrice,
|
|
468
|
+
bestPrice = takerIsLong
|
|
469
|
+
? BN.min(limitOrderPrice, bestPrice)
|
|
470
|
+
: BN.max(limitOrderPrice, bestPrice);
|
|
421
471
|
}
|
|
422
472
|
|
|
473
|
+
let worstPrice = bestPrice;
|
|
474
|
+
|
|
423
475
|
if (assetType === 'base') {
|
|
424
|
-
while (
|
|
476
|
+
while (
|
|
477
|
+
!cumulativeBaseFilled.eq(amount) &&
|
|
478
|
+
(ammLiquidity.gt(ZERO) || limitOrder)
|
|
479
|
+
) {
|
|
425
480
|
const limitOrderPrice = limitOrder?.getPrice(oraclePriceData, slot);
|
|
426
481
|
|
|
427
482
|
let maxAmmFill: BN;
|
|
@@ -442,11 +497,15 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
442
497
|
maxAmmFill = amount.sub(cumulativeBaseFilled);
|
|
443
498
|
}
|
|
444
499
|
|
|
500
|
+
maxAmmFill = BN.min(maxAmmFill, ammLiquidity);
|
|
501
|
+
|
|
445
502
|
if (maxAmmFill.gt(ZERO)) {
|
|
446
503
|
const baseFilled = BN.min(amount.sub(cumulativeBaseFilled), maxAmmFill);
|
|
447
504
|
const [afterSwapQuoteReserves, afterSwapBaseReserves] =
|
|
448
505
|
calculateAmmReservesAfterSwap(amm, 'base', baseFilled, swapDirection);
|
|
449
506
|
|
|
507
|
+
ammLiquidity = ammLiquidity.sub(baseFilled);
|
|
508
|
+
|
|
450
509
|
const quoteFilled = calculateQuoteAssetAmountSwapped(
|
|
451
510
|
amm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(),
|
|
452
511
|
amm.pegMultiplier,
|
|
@@ -459,11 +518,21 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
459
518
|
amm.baseAssetReserve = afterSwapBaseReserves;
|
|
460
519
|
amm.quoteAssetReserve = afterSwapQuoteReserves;
|
|
461
520
|
|
|
521
|
+
worstPrice = calculatePrice(
|
|
522
|
+
amm.baseAssetReserve,
|
|
523
|
+
amm.quoteAssetReserve,
|
|
524
|
+
amm.pegMultiplier
|
|
525
|
+
);
|
|
526
|
+
|
|
462
527
|
if (cumulativeBaseFilled.eq(amount)) {
|
|
463
528
|
break;
|
|
464
529
|
}
|
|
465
530
|
}
|
|
466
531
|
|
|
532
|
+
if (limitOrder && usersToSkip.has(limitOrder.userAccount)) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
467
536
|
const baseFilled = BN.min(
|
|
468
537
|
limitOrder.order.baseAssetAmount.sub(
|
|
469
538
|
limitOrder.order.baseAssetAmountFilled
|
|
@@ -475,6 +544,8 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
475
544
|
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
476
545
|
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
477
546
|
|
|
547
|
+
worstPrice = limitOrderPrice;
|
|
548
|
+
|
|
478
549
|
if (cumulativeBaseFilled.eq(amount)) {
|
|
479
550
|
break;
|
|
480
551
|
}
|
|
@@ -482,7 +553,10 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
482
553
|
limitOrder = limitOrders.next().value;
|
|
483
554
|
}
|
|
484
555
|
} else {
|
|
485
|
-
while (
|
|
556
|
+
while (
|
|
557
|
+
!cumulativeQuoteFilled.eq(amount) &&
|
|
558
|
+
(ammLiquidity.gt(ZERO) || limitOrder)
|
|
559
|
+
) {
|
|
486
560
|
const limitOrderPrice = limitOrder?.getPrice(oraclePriceData, slot);
|
|
487
561
|
|
|
488
562
|
let maxAmmFill: BN;
|
|
@@ -503,6 +577,8 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
503
577
|
maxAmmFill = amount.sub(cumulativeQuoteFilled);
|
|
504
578
|
}
|
|
505
579
|
|
|
580
|
+
maxAmmFill = BN.min(maxAmmFill, ammLiquidity);
|
|
581
|
+
|
|
506
582
|
if (maxAmmFill.gt(ZERO)) {
|
|
507
583
|
const quoteFilled = BN.min(
|
|
508
584
|
amount.sub(cumulativeQuoteFilled),
|
|
@@ -516,6 +592,8 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
516
592
|
swapDirection
|
|
517
593
|
);
|
|
518
594
|
|
|
595
|
+
ammLiquidity = ammLiquidity.sub(quoteFilled);
|
|
596
|
+
|
|
519
597
|
const baseFilled = afterSwapBaseReserves
|
|
520
598
|
.sub(amm.baseAssetReserve)
|
|
521
599
|
.abs();
|
|
@@ -526,11 +604,21 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
526
604
|
amm.baseAssetReserve = afterSwapBaseReserves;
|
|
527
605
|
amm.quoteAssetReserve = afterSwapQuoteReserves;
|
|
528
606
|
|
|
607
|
+
worstPrice = calculatePrice(
|
|
608
|
+
amm.baseAssetReserve,
|
|
609
|
+
amm.quoteAssetReserve,
|
|
610
|
+
amm.pegMultiplier
|
|
611
|
+
);
|
|
612
|
+
|
|
529
613
|
if (cumulativeQuoteFilled.eq(amount)) {
|
|
530
614
|
break;
|
|
531
615
|
}
|
|
532
616
|
}
|
|
533
617
|
|
|
618
|
+
if (limitOrder && usersToSkip.has(limitOrder.userAccount)) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
534
622
|
const quoteFilled = BN.min(
|
|
535
623
|
limitOrder.order.baseAssetAmount
|
|
536
624
|
.sub(limitOrder.order.baseAssetAmountFilled)
|
|
@@ -544,6 +632,8 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
544
632
|
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
545
633
|
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
546
634
|
|
|
635
|
+
worstPrice = limitOrderPrice;
|
|
636
|
+
|
|
547
637
|
if (cumulativeQuoteFilled.eq(amount)) {
|
|
548
638
|
break;
|
|
549
639
|
}
|
|
@@ -557,10 +647,226 @@ export function calculateEstimatedPerpEntryPrice(
|
|
|
557
647
|
.div(cumulativeBaseFilled);
|
|
558
648
|
|
|
559
649
|
const priceImpact = entryPrice
|
|
560
|
-
.sub(
|
|
650
|
+
.sub(bestPrice)
|
|
651
|
+
.mul(PRICE_PRECISION)
|
|
652
|
+
.div(bestPrice)
|
|
653
|
+
.abs();
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
entryPrice,
|
|
657
|
+
priceImpact,
|
|
658
|
+
bestPrice,
|
|
659
|
+
worstPrice,
|
|
660
|
+
baseFilled: cumulativeBaseFilled,
|
|
661
|
+
quoteFilled: cumulativeQuoteFilled,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Calculates the estimated entry price and price impact of order, in base or quote
|
|
667
|
+
* Price impact is based on the difference between the entry price and the best bid/ask price (whether it's dlob or serum)
|
|
668
|
+
*
|
|
669
|
+
* @param assetType
|
|
670
|
+
* @param amount
|
|
671
|
+
* @param direction
|
|
672
|
+
* @param market
|
|
673
|
+
* @param oraclePriceData
|
|
674
|
+
* @param dlob
|
|
675
|
+
* @param serumBids
|
|
676
|
+
* @param serumAsks
|
|
677
|
+
* @param slot
|
|
678
|
+
* @param usersToSkip
|
|
679
|
+
*/
|
|
680
|
+
export function calculateEstimatedSpotEntryPrice(
|
|
681
|
+
assetType: AssetType,
|
|
682
|
+
amount: BN,
|
|
683
|
+
direction: PositionDirection,
|
|
684
|
+
market: SpotMarketAccount,
|
|
685
|
+
oraclePriceData: OraclePriceData,
|
|
686
|
+
dlob: DLOB,
|
|
687
|
+
serumBids: Orderbook,
|
|
688
|
+
serumAsks: Orderbook,
|
|
689
|
+
slot: number,
|
|
690
|
+
usersToSkip = new Map<PublicKey, boolean>()
|
|
691
|
+
): {
|
|
692
|
+
entryPrice: BN;
|
|
693
|
+
priceImpact: BN;
|
|
694
|
+
bestPrice: BN;
|
|
695
|
+
worstPrice: BN;
|
|
696
|
+
baseFilled: BN;
|
|
697
|
+
quoteFilled: BN;
|
|
698
|
+
} {
|
|
699
|
+
if (amount.eq(ZERO)) {
|
|
700
|
+
return {
|
|
701
|
+
entryPrice: ZERO,
|
|
702
|
+
priceImpact: ZERO,
|
|
703
|
+
bestPrice: ZERO,
|
|
704
|
+
worstPrice: ZERO,
|
|
705
|
+
baseFilled: ZERO,
|
|
706
|
+
quoteFilled: ZERO,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const basePrecision = new BN(Math.pow(10, market.decimals));
|
|
711
|
+
|
|
712
|
+
const takerIsLong = isVariant(direction, 'long');
|
|
713
|
+
const dlobLimitOrders = dlob[
|
|
714
|
+
takerIsLong ? 'getMakerLimitAsks' : 'getMakerLimitBids'
|
|
715
|
+
](market.marketIndex, slot, MarketType.SPOT, oraclePriceData);
|
|
716
|
+
const serumLimitOrders = takerIsLong
|
|
717
|
+
? serumAsks.getL2(100)
|
|
718
|
+
: serumBids.getL2(100);
|
|
719
|
+
|
|
720
|
+
let cumulativeBaseFilled = ZERO;
|
|
721
|
+
let cumulativeQuoteFilled = ZERO;
|
|
722
|
+
|
|
723
|
+
let dlobLimitOrder = dlobLimitOrders.next().value;
|
|
724
|
+
let serumLimitOrder = serumLimitOrders.shift();
|
|
725
|
+
|
|
726
|
+
const dlobLimitOrderPrice = dlobLimitOrder?.getPrice(oraclePriceData, slot);
|
|
727
|
+
const serumLimitOrderPrice = serumLimitOrder
|
|
728
|
+
? new BN(serumLimitOrder[0] * PRICE_PRECISION.toNumber())
|
|
729
|
+
: undefined;
|
|
730
|
+
|
|
731
|
+
const bestPrice = takerIsLong
|
|
732
|
+
? BN.min(serumLimitOrderPrice || BN_MAX, dlobLimitOrderPrice || BN_MAX)
|
|
733
|
+
: BN.max(serumLimitOrderPrice || ZERO, dlobLimitOrderPrice || ZERO);
|
|
734
|
+
let worstPrice = bestPrice;
|
|
735
|
+
|
|
736
|
+
if (assetType === 'base') {
|
|
737
|
+
while (
|
|
738
|
+
!cumulativeBaseFilled.eq(amount) &&
|
|
739
|
+
(dlobLimitOrder || serumLimitOrder)
|
|
740
|
+
) {
|
|
741
|
+
const dlobLimitOrderPrice = dlobLimitOrder?.getPrice(
|
|
742
|
+
oraclePriceData,
|
|
743
|
+
slot
|
|
744
|
+
);
|
|
745
|
+
const serumLimitOrderPrice = serumLimitOrder
|
|
746
|
+
? new BN(serumLimitOrder[0] * PRICE_PRECISION.toNumber())
|
|
747
|
+
: undefined;
|
|
748
|
+
|
|
749
|
+
const useSerum = takerIsLong
|
|
750
|
+
? (serumLimitOrderPrice || BN_MAX).lt(dlobLimitOrderPrice || BN_MAX)
|
|
751
|
+
: (serumLimitOrderPrice || ZERO).gt(dlobLimitOrderPrice || ZERO);
|
|
752
|
+
|
|
753
|
+
if (!useSerum) {
|
|
754
|
+
if (dlobLimitOrder && usersToSkip.has(dlobLimitOrder.userAccount)) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const baseFilled = BN.min(
|
|
759
|
+
dlobLimitOrder.order.baseAssetAmount.sub(
|
|
760
|
+
dlobLimitOrder.order.baseAssetAmountFilled
|
|
761
|
+
),
|
|
762
|
+
amount.sub(cumulativeBaseFilled)
|
|
763
|
+
);
|
|
764
|
+
const quoteFilled = baseFilled
|
|
765
|
+
.mul(dlobLimitOrderPrice)
|
|
766
|
+
.div(basePrecision);
|
|
767
|
+
|
|
768
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
769
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
770
|
+
|
|
771
|
+
worstPrice = dlobLimitOrder;
|
|
772
|
+
|
|
773
|
+
dlobLimitOrder = dlobLimitOrders.next().value;
|
|
774
|
+
} else {
|
|
775
|
+
const baseFilled = BN.min(
|
|
776
|
+
new BN(serumLimitOrder[1] * basePrecision.toNumber()),
|
|
777
|
+
amount.sub(cumulativeBaseFilled)
|
|
778
|
+
);
|
|
779
|
+
const quoteFilled = baseFilled
|
|
780
|
+
.mul(serumLimitOrderPrice)
|
|
781
|
+
.div(basePrecision);
|
|
782
|
+
|
|
783
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
784
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
785
|
+
|
|
786
|
+
worstPrice = serumLimitOrderPrice;
|
|
787
|
+
|
|
788
|
+
serumLimitOrder = serumLimitOrders.shift();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
while (
|
|
793
|
+
!cumulativeQuoteFilled.eq(amount) &&
|
|
794
|
+
(dlobLimitOrder || serumLimitOrder)
|
|
795
|
+
) {
|
|
796
|
+
const dlobLimitOrderPrice = dlobLimitOrder?.getPrice(
|
|
797
|
+
oraclePriceData,
|
|
798
|
+
slot
|
|
799
|
+
);
|
|
800
|
+
const serumLimitOrderPrice = serumLimitOrder
|
|
801
|
+
? new BN(serumLimitOrder[0] * PRICE_PRECISION.toNumber())
|
|
802
|
+
: undefined;
|
|
803
|
+
|
|
804
|
+
const useSerum = takerIsLong
|
|
805
|
+
? (serumLimitOrderPrice || BN_MAX).lt(dlobLimitOrderPrice || BN_MAX)
|
|
806
|
+
: (serumLimitOrderPrice || ZERO).gt(dlobLimitOrderPrice || ZERO);
|
|
807
|
+
|
|
808
|
+
if (!useSerum) {
|
|
809
|
+
if (dlobLimitOrder && usersToSkip.has(dlobLimitOrder.userAccount)) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const quoteFilled = BN.min(
|
|
814
|
+
dlobLimitOrder.order.baseAssetAmount
|
|
815
|
+
.sub(dlobLimitOrder.order.baseAssetAmountFilled)
|
|
816
|
+
.mul(dlobLimitOrderPrice)
|
|
817
|
+
.div(basePrecision),
|
|
818
|
+
amount.sub(cumulativeQuoteFilled)
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
const baseFilled = quoteFilled
|
|
822
|
+
.mul(basePrecision)
|
|
823
|
+
.div(dlobLimitOrderPrice);
|
|
824
|
+
|
|
825
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
826
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
827
|
+
|
|
828
|
+
worstPrice = dlobLimitOrderPrice;
|
|
829
|
+
|
|
830
|
+
dlobLimitOrder = dlobLimitOrders.next().value;
|
|
831
|
+
} else {
|
|
832
|
+
const serumOrderBaseAmount = new BN(
|
|
833
|
+
serumLimitOrder[1] * basePrecision.toNumber()
|
|
834
|
+
);
|
|
835
|
+
const quoteFilled = BN.min(
|
|
836
|
+
serumOrderBaseAmount.mul(serumLimitOrderPrice).div(basePrecision),
|
|
837
|
+
amount.sub(cumulativeQuoteFilled)
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
const baseFilled = quoteFilled
|
|
841
|
+
.mul(basePrecision)
|
|
842
|
+
.div(serumLimitOrderPrice);
|
|
843
|
+
|
|
844
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
845
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
846
|
+
|
|
847
|
+
worstPrice = serumLimitOrderPrice;
|
|
848
|
+
|
|
849
|
+
serumLimitOrder = serumLimitOrders.shift();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const entryPrice = cumulativeQuoteFilled
|
|
855
|
+
.mul(basePrecision)
|
|
856
|
+
.div(cumulativeBaseFilled);
|
|
857
|
+
|
|
858
|
+
const priceImpact = entryPrice
|
|
859
|
+
.sub(bestPrice)
|
|
561
860
|
.mul(PRICE_PRECISION)
|
|
562
|
-
.div(
|
|
861
|
+
.div(bestPrice)
|
|
563
862
|
.abs();
|
|
564
863
|
|
|
565
|
-
return
|
|
864
|
+
return {
|
|
865
|
+
entryPrice,
|
|
866
|
+
priceImpact,
|
|
867
|
+
bestPrice,
|
|
868
|
+
worstPrice,
|
|
869
|
+
baseFilled: cumulativeBaseFilled,
|
|
870
|
+
quoteFilled: cumulativeQuoteFilled,
|
|
871
|
+
};
|
|
566
872
|
}
|