@drift-labs/sdk 2.11.0 → 2.12.0-beta.1
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/README.md +63 -48
- package/lib/dlob/DLOB.d.ts +15 -0
- package/lib/dlob/DLOB.js +30 -0
- package/lib/examples/makeTradeExample.js +18 -20
- package/lib/idl/drift.json +6 -1
- package/lib/math/trade.d.ts +16 -1
- package/lib/math/trade.js +135 -1
- package/package.json +1 -1
- package/src/dlob/DLOB.ts +61 -0
- package/src/examples/makeTradeExample.ts +32 -40
- package/src/idl/drift.json +6 -1
- package/src/math/trade.ts +217 -2
package/README.md
CHANGED
|
@@ -11,14 +11,6 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
# Drift Protocol v2
|
|
15
|
-
|
|
16
|
-
This repository provides open source access to Drift's Typescript SDK, Solana Programs, and more.
|
|
17
|
-
|
|
18
|
-
# SDK Guide
|
|
19
|
-
|
|
20
|
-
The technical documentation for the SDK can be found [here](https://drift-labs.github.io/protocol-v2/sdk/), and you can visit Drift's general purpose documentation [here](https://docs.drift.trade/sdk-documentation).
|
|
21
|
-
|
|
22
14
|
## Installation
|
|
23
15
|
|
|
24
16
|
```
|
|
@@ -84,7 +76,7 @@ convertToNumber(new BN(10500), new BN(1000)); // = 10.5
|
|
|
84
76
|
### Setting up an account and making a trade
|
|
85
77
|
|
|
86
78
|
```typescript
|
|
87
|
-
import {
|
|
79
|
+
import { AnchorProvider, BN } from '@project-serum/anchor';
|
|
88
80
|
import { Token, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
89
81
|
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
90
82
|
import {
|
|
@@ -92,14 +84,18 @@ import {
|
|
|
92
84
|
DriftClient,
|
|
93
85
|
User,
|
|
94
86
|
initialize,
|
|
95
|
-
Markets,
|
|
96
87
|
PositionDirection,
|
|
97
88
|
convertToNumber,
|
|
98
89
|
calculateTradeSlippage,
|
|
99
90
|
PRICE_PRECISION,
|
|
100
91
|
QUOTE_PRECISION,
|
|
101
92
|
Wallet,
|
|
102
|
-
|
|
93
|
+
PerpMarkets,
|
|
94
|
+
BASE_PRECISION,
|
|
95
|
+
getMarketOrderParams,
|
|
96
|
+
BulkAccountLoader,
|
|
97
|
+
getMarketsAndOraclesForSubscription
|
|
98
|
+
} from '../sdk';
|
|
103
99
|
|
|
104
100
|
export const getTokenAddress = (
|
|
105
101
|
mintAddress: string,
|
|
@@ -114,8 +110,9 @@ export const getTokenAddress = (
|
|
|
114
110
|
};
|
|
115
111
|
|
|
116
112
|
const main = async () => {
|
|
113
|
+
const env = 'devnet';
|
|
117
114
|
// Initialize Drift SDK
|
|
118
|
-
const sdkConfig = initialize({ env
|
|
115
|
+
const sdkConfig = initialize({ env });
|
|
119
116
|
|
|
120
117
|
// Set up the Wallet and Provider
|
|
121
118
|
const privateKey = process.env.BOT_PRIVATE_KEY; // stored as an array string
|
|
@@ -129,7 +126,11 @@ const main = async () => {
|
|
|
129
126
|
const connection = new Connection(rpcAddress);
|
|
130
127
|
|
|
131
128
|
// Set up the Provider
|
|
132
|
-
const provider = new
|
|
129
|
+
const provider = new AnchorProvider(
|
|
130
|
+
connection,
|
|
131
|
+
wallet,
|
|
132
|
+
AnchorProvider.defaultOptions()
|
|
133
|
+
);
|
|
133
134
|
|
|
134
135
|
// Check SOL Balance
|
|
135
136
|
const lamportsBalance = await connection.getBalance(wallet.publicKey);
|
|
@@ -142,18 +143,35 @@ const main = async () => {
|
|
|
142
143
|
);
|
|
143
144
|
|
|
144
145
|
// Set up the Drift Client
|
|
145
|
-
const
|
|
146
|
-
const
|
|
146
|
+
const driftPublicKey = new PublicKey(sdkConfig.DRIFT_PROGRAM_ID);
|
|
147
|
+
const bulkAccountLoader = new BulkAccountLoader(
|
|
147
148
|
connection,
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
'confirmed',
|
|
150
|
+
1000
|
|
150
151
|
);
|
|
152
|
+
const driftClient = new DriftClient({
|
|
153
|
+
connection,
|
|
154
|
+
wallet: provider.wallet,
|
|
155
|
+
programID: driftPublicKey,
|
|
156
|
+
...getMarketsAndOraclesForSubscription(env),
|
|
157
|
+
accountSubscription: {
|
|
158
|
+
type: 'polling',
|
|
159
|
+
accountLoader: bulkAccountLoader,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
151
162
|
await driftClient.subscribe();
|
|
152
163
|
|
|
153
|
-
// Set up
|
|
154
|
-
const user = User
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
// Set up user client
|
|
165
|
+
const user = new User({
|
|
166
|
+
driftClient: driftClient,
|
|
167
|
+
userAccountPublicKey: await driftClient.getUserAccountPublicKey(),
|
|
168
|
+
accountSubscription: {
|
|
169
|
+
type: 'polling',
|
|
170
|
+
accountLoader: bulkAccountLoader,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
//// Check if user account exists for the current wallet
|
|
157
175
|
const userAccountExists = await user.exists();
|
|
158
176
|
|
|
159
177
|
if (!userAccountExists) {
|
|
@@ -171,51 +189,48 @@ const main = async () => {
|
|
|
171
189
|
await user.subscribe();
|
|
172
190
|
|
|
173
191
|
// Get current price
|
|
174
|
-
const solMarketInfo =
|
|
192
|
+
const solMarketInfo = PerpMarkets[env].find(
|
|
175
193
|
(market) => market.baseAssetSymbol === 'SOL'
|
|
176
194
|
);
|
|
177
195
|
|
|
178
|
-
const
|
|
179
|
-
driftClient.
|
|
196
|
+
const [bid, ask] = calculateBidAskPrice(
|
|
197
|
+
driftClient.getPerpMarketAccount(marketIndex).amm,
|
|
198
|
+
driftClient.getOracleDataForPerpMarket(marketIndex)
|
|
180
199
|
);
|
|
181
200
|
|
|
182
|
-
const
|
|
201
|
+
const formattedBidPrice = convertToNumber(bid, PRICE_PRECISION);
|
|
202
|
+
const formattedAskPrice = convertToNumber(ask, PRICE_PRECISION);
|
|
183
203
|
|
|
184
|
-
console.log(
|
|
204
|
+
console.log(
|
|
205
|
+
`Current amm bid and ask price are $${formattedBidPrice} and $${formattedAskPrice}`
|
|
206
|
+
);
|
|
185
207
|
|
|
186
208
|
// Estimate the slippage for a $5000 LONG trade
|
|
187
|
-
const solMarketAccount = driftClient.
|
|
209
|
+
const solMarketAccount = driftClient.getPerpMarketAccount(
|
|
210
|
+
solMarketInfo.marketIndex
|
|
211
|
+
);
|
|
188
212
|
|
|
189
213
|
const slippage = convertToNumber(
|
|
190
214
|
calculateTradeSlippage(
|
|
191
215
|
PositionDirection.LONG,
|
|
192
|
-
new BN(
|
|
193
|
-
solMarketAccount
|
|
216
|
+
new BN(1).mul(BASE_PRECISION),
|
|
217
|
+
solMarketAccount,
|
|
218
|
+
'base',
|
|
219
|
+
driftClient.getOracleDataForPerpMarket(solMarketInfo.marketIndex)
|
|
194
220
|
)[0],
|
|
195
221
|
PRICE_PRECISION
|
|
196
222
|
);
|
|
197
223
|
|
|
198
|
-
console.log(
|
|
199
|
-
`Slippage for a $5000 LONG on the SOL market would be $${slippage}`
|
|
200
|
-
);
|
|
224
|
+
console.log(`Slippage for a 1 SOL-PERP would be $${slippage}`);
|
|
201
225
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
await driftClient.placePerpOrder(
|
|
227
|
+
getMarketOrderParams({
|
|
228
|
+
baseAssetAmount: new BN(1).mul(BASE_PRECISION),
|
|
229
|
+
direction: PositionDirection.LONG,
|
|
230
|
+
marketIndex: solMarketAccount.marketIndex,
|
|
231
|
+
})
|
|
207
232
|
);
|
|
208
|
-
console.log(`
|
|
209
|
-
|
|
210
|
-
// Reduce the position by $2000
|
|
211
|
-
await driftClient.openPosition(
|
|
212
|
-
PositionDirection.SHORT,
|
|
213
|
-
new BN(2000).mul(QUOTE_PRECISION),
|
|
214
|
-
solMarketInfo.marketIndex
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
// Close the rest of the position
|
|
218
|
-
await driftClient.closePosition(solMarketInfo.marketIndex);
|
|
233
|
+
console.log(`Placed a 1 SOL-PERP LONG order`);
|
|
219
234
|
};
|
|
220
235
|
|
|
221
236
|
main();
|
package/lib/dlob/DLOB.d.ts
CHANGED
|
@@ -64,7 +64,22 @@ export declare class DLOB {
|
|
|
64
64
|
getMarketAsks(marketIndex: number, marketType: MarketType): Generator<DLOBNode>;
|
|
65
65
|
private getBestNode;
|
|
66
66
|
getLimitAsks(marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData): Generator<DLOBNode>;
|
|
67
|
+
/**
|
|
68
|
+
* Filters the limit asks that are post only or have been place for sufficiently long
|
|
69
|
+
* Useful for displaying order book that doesn't have taker limit orders crossing spread
|
|
70
|
+
*
|
|
71
|
+
* @returns
|
|
72
|
+
*/
|
|
73
|
+
getRestingLimitAsks(marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData, minPerpAuctionDuration: number): Generator<DLOBNode>;
|
|
74
|
+
isRestingLimitOrder(order: Order, slot: number, minPerpAuctionDuration: number): boolean;
|
|
67
75
|
getLimitBids(marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData): Generator<DLOBNode>;
|
|
76
|
+
/**
|
|
77
|
+
* Filters the limit bids that are post only or have been place for sufficiently long
|
|
78
|
+
* Useful for displaying order book that doesn't have taker limit orders crossing spread
|
|
79
|
+
*
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
82
|
+
getRestingLimitBids(marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData, minPerpAuctionDuration: number): Generator<DLOBNode>;
|
|
68
83
|
getAsks(marketIndex: number, fallbackAsk: BN | undefined, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData): Generator<DLOBNode>;
|
|
69
84
|
getBids(marketIndex: number, fallbackBid: BN | undefined, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData): Generator<DLOBNode>;
|
|
70
85
|
findCrossingLimitOrders(marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData, fallbackAsk: BN | undefined, fallbackBid: BN | undefined): NodeToFill[];
|
package/lib/dlob/DLOB.js
CHANGED
|
@@ -535,6 +535,23 @@ class DLOB {
|
|
|
535
535
|
return bestPrice.lt(currentPrice);
|
|
536
536
|
});
|
|
537
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Filters the limit asks that are post only or have been place for sufficiently long
|
|
540
|
+
* Useful for displaying order book that doesn't have taker limit orders crossing spread
|
|
541
|
+
*
|
|
542
|
+
* @returns
|
|
543
|
+
*/
|
|
544
|
+
*getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData, minPerpAuctionDuration) {
|
|
545
|
+
for (const node of this.getLimitAsks(marketIndex, slot, marketType, oraclePriceData)) {
|
|
546
|
+
if (this.isRestingLimitOrder(node.order, slot, minPerpAuctionDuration)) {
|
|
547
|
+
yield node;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
isRestingLimitOrder(order, slot, minPerpAuctionDuration) {
|
|
552
|
+
return (order.postOnly ||
|
|
553
|
+
new __1.BN(slot).sub(order.slot).gte(new __1.BN(minPerpAuctionDuration * 1.5)));
|
|
554
|
+
}
|
|
538
555
|
*getLimitBids(marketIndex, slot, marketType, oraclePriceData) {
|
|
539
556
|
if ((0, __1.isVariant)(marketType, 'spot') && !oraclePriceData) {
|
|
540
557
|
throw new Error('Must provide OraclePriceData to get spot bids');
|
|
@@ -552,6 +569,19 @@ class DLOB {
|
|
|
552
569
|
return bestPrice.gt(currentPrice);
|
|
553
570
|
});
|
|
554
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Filters the limit bids that are post only or have been place for sufficiently long
|
|
574
|
+
* Useful for displaying order book that doesn't have taker limit orders crossing spread
|
|
575
|
+
*
|
|
576
|
+
* @returns
|
|
577
|
+
*/
|
|
578
|
+
*getRestingLimitBids(marketIndex, slot, marketType, oraclePriceData, minPerpAuctionDuration) {
|
|
579
|
+
for (const node of this.getLimitBids(marketIndex, slot, marketType, oraclePriceData)) {
|
|
580
|
+
if (this.isRestingLimitOrder(node.order, slot, minPerpAuctionDuration)) {
|
|
581
|
+
yield node;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
555
585
|
*getAsks(marketIndex, fallbackAsk, slot, marketType, oraclePriceData) {
|
|
556
586
|
if ((0, __1.isVariant)(marketType, 'spot') && !oraclePriceData) {
|
|
557
587
|
throw new Error('Must provide OraclePriceData to get spot asks');
|
|
@@ -11,10 +11,10 @@ const getTokenAddress = (mintAddress, userPubKey) => {
|
|
|
11
11
|
return spl_token_1.Token.getAssociatedTokenAddress(new web3_js_1.PublicKey(`ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL`), spl_token_1.TOKEN_PROGRAM_ID, new web3_js_1.PublicKey(mintAddress), new web3_js_1.PublicKey(userPubKey));
|
|
12
12
|
};
|
|
13
13
|
exports.getTokenAddress = getTokenAddress;
|
|
14
|
-
const
|
|
14
|
+
const env = 'devnet';
|
|
15
15
|
const main = async () => {
|
|
16
16
|
// Initialize Drift SDK
|
|
17
|
-
const sdkConfig = (0, __2.initialize)({ env
|
|
17
|
+
const sdkConfig = (0, __2.initialize)({ env });
|
|
18
18
|
// Set up the Wallet and Provider
|
|
19
19
|
const privateKey = process.env.BOT_PRIVATE_KEY; // stored as an array string
|
|
20
20
|
const keypair = web3_js_1.Keypair.fromSecretKey(Uint8Array.from(JSON.parse(privateKey)));
|
|
@@ -36,15 +36,14 @@ const main = async () => {
|
|
|
36
36
|
connection,
|
|
37
37
|
wallet: provider.wallet,
|
|
38
38
|
programID: driftPublicKey,
|
|
39
|
-
|
|
40
|
-
spotMarketIndexes: spotMarkets_1.SpotMarkets[cluster].map((spotMarket) => spotMarket.marketIndex),
|
|
39
|
+
...(0, __2.getMarketsAndOraclesForSubscription)(env),
|
|
41
40
|
accountSubscription: {
|
|
42
41
|
type: 'polling',
|
|
43
42
|
accountLoader: bulkAccountLoader,
|
|
44
43
|
},
|
|
45
44
|
});
|
|
46
45
|
await driftClient.subscribe();
|
|
47
|
-
// Set up
|
|
46
|
+
// Set up user client
|
|
48
47
|
const user = new __2.User({
|
|
49
48
|
driftClient: driftClient,
|
|
50
49
|
userAccountPublicKey: await driftClient.getUserAccountPublicKey(),
|
|
@@ -53,7 +52,7 @@ const main = async () => {
|
|
|
53
52
|
accountLoader: bulkAccountLoader,
|
|
54
53
|
},
|
|
55
54
|
});
|
|
56
|
-
//// Check if
|
|
55
|
+
//// Check if user account exists for the current wallet
|
|
57
56
|
const userAccountExists = await user.exists();
|
|
58
57
|
if (!userAccountExists) {
|
|
59
58
|
//// Create a Clearing House account by Depositing some USDC ($10,000 in this case)
|
|
@@ -63,21 +62,20 @@ const main = async () => {
|
|
|
63
62
|
await user.subscribe();
|
|
64
63
|
// Get current price
|
|
65
64
|
const solMarketInfo = sdkConfig.PERP_MARKETS.find((market) => market.baseAssetSymbol === 'SOL');
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
65
|
+
const marketIndex = solMarketInfo.marketIndex;
|
|
66
|
+
const [bid, ask] = (0, __1.calculateBidAskPrice)(driftClient.getPerpMarketAccount(marketIndex).amm, driftClient.getOracleDataForPerpMarket(marketIndex));
|
|
67
|
+
const formattedBidPrice = (0, __2.convertToNumber)(bid, __2.PRICE_PRECISION);
|
|
68
|
+
const formattedAskPrice = (0, __2.convertToNumber)(ask, __2.PRICE_PRECISION);
|
|
69
|
+
console.log(`Current amm bid and ask price are $${formattedBidPrice} and $${formattedAskPrice}`);
|
|
69
70
|
// Estimate the slippage for a $5000 LONG trade
|
|
70
71
|
const solMarketAccount = driftClient.getPerpMarketAccount(solMarketInfo.marketIndex);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await driftClient.openPosition(__2.PositionDirection.SHORT, reduceAmount, solMarketInfo.marketIndex);
|
|
80
|
-
// Close the rest of the position
|
|
81
|
-
await driftClient.closePosition(solMarketInfo.marketIndex);
|
|
72
|
+
const slippage = (0, __2.convertToNumber)((0, __2.calculateTradeSlippage)(__2.PositionDirection.LONG, new anchor_1.BN(1).mul(__1.BASE_PRECISION), solMarketAccount, 'base', driftClient.getOracleDataForPerpMarket(solMarketInfo.marketIndex))[0], __2.PRICE_PRECISION);
|
|
73
|
+
console.log(`Slippage for a 1 SOL-PERP would be $${slippage}`);
|
|
74
|
+
await driftClient.placePerpOrder((0, __1.getMarketOrderParams)({
|
|
75
|
+
baseAssetAmount: new anchor_1.BN(1).mul(__1.BASE_PRECISION),
|
|
76
|
+
direction: __2.PositionDirection.LONG,
|
|
77
|
+
marketIndex: solMarketAccount.marketIndex,
|
|
78
|
+
}));
|
|
79
|
+
console.log(`Placed a 1 SOL-PERP LONG order`);
|
|
82
80
|
};
|
|
83
81
|
main();
|
package/lib/idl/drift.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.12.0-beta.1",
|
|
3
3
|
"name": "drift",
|
|
4
4
|
"instructions": [
|
|
5
5
|
{
|
|
@@ -8568,6 +8568,11 @@
|
|
|
8568
8568
|
"code": 6224,
|
|
8569
8569
|
"name": "MaxNumberOfUsers",
|
|
8570
8570
|
"msg": "Max Number Of Users"
|
|
8571
|
+
},
|
|
8572
|
+
{
|
|
8573
|
+
"code": 6225,
|
|
8574
|
+
"name": "InvalidOracleForSettlePnl",
|
|
8575
|
+
"msg": "InvalidOracleForSettlePnl"
|
|
8571
8576
|
}
|
|
8572
8577
|
]
|
|
8573
8578
|
}
|
package/lib/math/trade.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { PerpMarketAccount, PositionDirection } from '../types';
|
|
|
3
3
|
import { BN } from '@project-serum/anchor';
|
|
4
4
|
import { AssetType } from './amm';
|
|
5
5
|
import { OraclePriceData } from '../oracles/types';
|
|
6
|
+
import { DLOB } from '../dlob/DLOB';
|
|
6
7
|
export declare type PriceImpactUnit = 'entryPrice' | 'maxPrice' | 'priceDelta' | 'priceDeltaAsNumber' | 'pctAvg' | 'pctMax' | 'quoteAssetAmount' | 'quoteAssetAmountPeg' | 'acquiredBaseAssetAmount' | 'acquiredQuoteAssetAmount' | 'all';
|
|
7
8
|
/**
|
|
8
9
|
* Calculates avg/max slippage (price impact) for candidate trade
|
|
@@ -21,7 +22,7 @@ export declare type PriceImpactUnit = 'entryPrice' | 'maxPrice' | 'priceDelta' |
|
|
|
21
22
|
*
|
|
22
23
|
* 'newPrice' => the price of the asset after the trade : Precision PRICE_PRECISION
|
|
23
24
|
*/
|
|
24
|
-
export declare function calculateTradeSlippage(direction: PositionDirection, amount: BN, market: PerpMarketAccount, inputAssetType
|
|
25
|
+
export declare function calculateTradeSlippage(direction: PositionDirection, amount: BN, market: PerpMarketAccount, inputAssetType: AssetType, oraclePriceData: OraclePriceData, useSpread?: boolean): [BN, BN, BN, BN];
|
|
25
26
|
/**
|
|
26
27
|
* Calculates acquired amounts for trade executed
|
|
27
28
|
* @param direction
|
|
@@ -52,3 +53,17 @@ export declare function calculateTradeAcquiredAmounts(direction: PositionDirecti
|
|
|
52
53
|
* ]
|
|
53
54
|
*/
|
|
54
55
|
export declare function calculateTargetPriceTrade(market: PerpMarketAccount, targetPrice: BN, pct?: BN, outputAssetType?: AssetType, oraclePriceData?: OraclePriceData, useSpread?: boolean): [PositionDirection, BN, BN, BN];
|
|
56
|
+
/**
|
|
57
|
+
* Calculates the estimated entry price and price impact of order, in base or quote
|
|
58
|
+
* Price impact is based on the difference between the entry price and the best bid/ask price (whether it's dlob or vamm)
|
|
59
|
+
*
|
|
60
|
+
* @param assetType
|
|
61
|
+
* @param amount
|
|
62
|
+
* @param direction
|
|
63
|
+
* @param market
|
|
64
|
+
* @param oraclePriceData
|
|
65
|
+
* @param dlob
|
|
66
|
+
* @param slot
|
|
67
|
+
* @param minPerpAuctionDuration
|
|
68
|
+
*/
|
|
69
|
+
export declare function calculateEstimatedPerpEntryPrice(assetType: AssetType, amount: BN, direction: PositionDirection, market: PerpMarketAccount, oraclePriceData: OraclePriceData, dlob: DLOB, slot: number, minPerpAuctionDuration: number): [BN, BN];
|
package/lib/math/trade.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.calculateTargetPriceTrade = exports.calculateTradeAcquiredAmounts = exports.calculateTradeSlippage = void 0;
|
|
3
|
+
exports.calculateEstimatedPerpEntryPrice = exports.calculateTargetPriceTrade = exports.calculateTradeAcquiredAmounts = exports.calculateTradeSlippage = void 0;
|
|
4
4
|
const types_1 = require("../types");
|
|
5
5
|
const anchor_1 = require("@project-serum/anchor");
|
|
6
6
|
const assert_1 = require("../assert/assert");
|
|
@@ -247,3 +247,137 @@ function calculateTargetPriceTrade(market, targetPrice, pct = MAXPCT, outputAsse
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
exports.calculateTargetPriceTrade = calculateTargetPriceTrade;
|
|
250
|
+
/**
|
|
251
|
+
* Calculates the estimated entry price and price impact of order, in base or quote
|
|
252
|
+
* Price impact is based on the difference between the entry price and the best bid/ask price (whether it's dlob or vamm)
|
|
253
|
+
*
|
|
254
|
+
* @param assetType
|
|
255
|
+
* @param amount
|
|
256
|
+
* @param direction
|
|
257
|
+
* @param market
|
|
258
|
+
* @param oraclePriceData
|
|
259
|
+
* @param dlob
|
|
260
|
+
* @param slot
|
|
261
|
+
* @param minPerpAuctionDuration
|
|
262
|
+
*/
|
|
263
|
+
function calculateEstimatedPerpEntryPrice(assetType, amount, direction, market, oraclePriceData, dlob, slot, minPerpAuctionDuration) {
|
|
264
|
+
if (amount.eq(numericConstants_1.ZERO)) {
|
|
265
|
+
return [numericConstants_1.ZERO, numericConstants_1.ZERO];
|
|
266
|
+
}
|
|
267
|
+
const takerIsLong = (0, types_2.isVariant)(direction, 'long');
|
|
268
|
+
const limitOrders = dlob[takerIsLong ? 'getRestingLimitAsks' : 'getRestingLimitBids'](market.marketIndex, slot, types_1.MarketType.PERP, oraclePriceData, minPerpAuctionDuration);
|
|
269
|
+
const swapDirection = (0, amm_1.getSwapDirection)(assetType, direction);
|
|
270
|
+
const { baseAssetReserve, quoteAssetReserve, sqrtK, newPeg } = (0, amm_1.calculateUpdatedAMMSpreadReserves)(market.amm, direction, oraclePriceData);
|
|
271
|
+
const amm = {
|
|
272
|
+
baseAssetReserve,
|
|
273
|
+
quoteAssetReserve,
|
|
274
|
+
sqrtK: sqrtK,
|
|
275
|
+
pegMultiplier: newPeg,
|
|
276
|
+
};
|
|
277
|
+
const invariant = amm.sqrtK.mul(amm.sqrtK);
|
|
278
|
+
let initialPrice = (0, amm_1.calculatePrice)(amm.baseAssetReserve, amm.quoteAssetReserve, amm.pegMultiplier);
|
|
279
|
+
let cumulativeBaseFilled = numericConstants_1.ZERO;
|
|
280
|
+
let cumulativeQuoteFilled = numericConstants_1.ZERO;
|
|
281
|
+
let limitOrder = limitOrders.next().value;
|
|
282
|
+
if (limitOrder) {
|
|
283
|
+
const limitOrderPrice = limitOrder.getPrice(oraclePriceData, slot);
|
|
284
|
+
initialPrice = takerIsLong
|
|
285
|
+
? anchor_1.BN.min(limitOrderPrice, initialPrice)
|
|
286
|
+
: anchor_1.BN.max(limitOrderPrice, initialPrice);
|
|
287
|
+
}
|
|
288
|
+
if (assetType === 'base') {
|
|
289
|
+
while (!cumulativeBaseFilled.eq(amount)) {
|
|
290
|
+
const limitOrderPrice = limitOrder === null || limitOrder === void 0 ? void 0 : limitOrder.getPrice(oraclePriceData, slot);
|
|
291
|
+
let maxAmmFill;
|
|
292
|
+
if (limitOrderPrice) {
|
|
293
|
+
const newBaseReserves = (0, utils_1.squareRootBN)(invariant
|
|
294
|
+
.mul(numericConstants_1.PRICE_PRECISION)
|
|
295
|
+
.mul(amm.pegMultiplier)
|
|
296
|
+
.div(limitOrderPrice)
|
|
297
|
+
.div(numericConstants_1.PEG_PRECISION));
|
|
298
|
+
// will be zero if the limit order price is better than the amm price
|
|
299
|
+
maxAmmFill = takerIsLong
|
|
300
|
+
? amm.baseAssetReserve.sub(newBaseReserves)
|
|
301
|
+
: newBaseReserves.sub(amm.baseAssetReserve);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
maxAmmFill = amount.sub(cumulativeBaseFilled);
|
|
305
|
+
}
|
|
306
|
+
if (maxAmmFill.gt(numericConstants_1.ZERO)) {
|
|
307
|
+
const baseFilled = anchor_1.BN.min(amount.sub(cumulativeBaseFilled), maxAmmFill);
|
|
308
|
+
const [afterSwapQuoteReserves, afterSwapBaseReserves] = (0, amm_1.calculateAmmReservesAfterSwap)(amm, 'base', baseFilled, swapDirection);
|
|
309
|
+
const quoteFilled = (0, amm_1.calculateQuoteAssetAmountSwapped)(amm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(), amm.pegMultiplier, swapDirection);
|
|
310
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
311
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
312
|
+
amm.baseAssetReserve = afterSwapBaseReserves;
|
|
313
|
+
amm.quoteAssetReserve = afterSwapQuoteReserves;
|
|
314
|
+
if (cumulativeBaseFilled.eq(amount)) {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const baseFilled = anchor_1.BN.min(limitOrder.order.baseAssetAmount.sub(limitOrder.order.baseAssetAmountFilled), amount.sub(cumulativeBaseFilled));
|
|
319
|
+
const quoteFilled = baseFilled.mul(limitOrderPrice).div(numericConstants_1.BASE_PRECISION);
|
|
320
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
321
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
322
|
+
if (cumulativeBaseFilled.eq(amount)) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
limitOrder = limitOrders.next().value;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
while (!cumulativeQuoteFilled.eq(amount)) {
|
|
330
|
+
const limitOrderPrice = limitOrder === null || limitOrder === void 0 ? void 0 : limitOrder.getPrice(oraclePriceData, slot);
|
|
331
|
+
let maxAmmFill;
|
|
332
|
+
if (limitOrderPrice) {
|
|
333
|
+
const newQuoteReserves = (0, utils_1.squareRootBN)(invariant
|
|
334
|
+
.mul(numericConstants_1.PEG_PRECISION)
|
|
335
|
+
.mul(limitOrderPrice)
|
|
336
|
+
.div(amm.pegMultiplier)
|
|
337
|
+
.div(numericConstants_1.PRICE_PRECISION));
|
|
338
|
+
// will be zero if the limit order price is better than the amm price
|
|
339
|
+
maxAmmFill = takerIsLong
|
|
340
|
+
? newQuoteReserves.sub(amm.quoteAssetReserve)
|
|
341
|
+
: amm.quoteAssetReserve.sub(newQuoteReserves);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
maxAmmFill = amount.sub(cumulativeQuoteFilled);
|
|
345
|
+
}
|
|
346
|
+
if (maxAmmFill.gt(numericConstants_1.ZERO)) {
|
|
347
|
+
const quoteFilled = anchor_1.BN.min(amount.sub(cumulativeQuoteFilled), maxAmmFill);
|
|
348
|
+
const [afterSwapQuoteReserves, afterSwapBaseReserves] = (0, amm_1.calculateAmmReservesAfterSwap)(amm, 'quote', quoteFilled, swapDirection);
|
|
349
|
+
const baseFilled = afterSwapBaseReserves
|
|
350
|
+
.sub(amm.baseAssetReserve)
|
|
351
|
+
.abs();
|
|
352
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
353
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
354
|
+
amm.baseAssetReserve = afterSwapBaseReserves;
|
|
355
|
+
amm.quoteAssetReserve = afterSwapQuoteReserves;
|
|
356
|
+
if (cumulativeQuoteFilled.eq(amount)) {
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const quoteFilled = anchor_1.BN.min(limitOrder.order.baseAssetAmount
|
|
361
|
+
.sub(limitOrder.order.baseAssetAmountFilled)
|
|
362
|
+
.mul(limitOrderPrice)
|
|
363
|
+
.div(numericConstants_1.BASE_PRECISION), amount.sub(cumulativeQuoteFilled));
|
|
364
|
+
const baseFilled = quoteFilled.mul(numericConstants_1.BASE_PRECISION).div(limitOrderPrice);
|
|
365
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
366
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
367
|
+
if (cumulativeQuoteFilled.eq(amount)) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
limitOrder = limitOrders.next().value;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const entryPrice = cumulativeQuoteFilled
|
|
374
|
+
.mul(numericConstants_1.BASE_PRECISION)
|
|
375
|
+
.div(cumulativeBaseFilled);
|
|
376
|
+
const priceImpact = entryPrice
|
|
377
|
+
.sub(initialPrice)
|
|
378
|
+
.mul(numericConstants_1.PRICE_PRECISION)
|
|
379
|
+
.div(initialPrice)
|
|
380
|
+
.abs();
|
|
381
|
+
return [entryPrice, priceImpact];
|
|
382
|
+
}
|
|
383
|
+
exports.calculateEstimatedPerpEntryPrice = calculateEstimatedPerpEntryPrice;
|
package/package.json
CHANGED
package/src/dlob/DLOB.ts
CHANGED
|
@@ -915,6 +915,42 @@ export class DLOB {
|
|
|
915
915
|
);
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
+
/**
|
|
919
|
+
* Filters the limit asks that are post only or have been place for sufficiently long
|
|
920
|
+
* Useful for displaying order book that doesn't have taker limit orders crossing spread
|
|
921
|
+
*
|
|
922
|
+
* @returns
|
|
923
|
+
*/
|
|
924
|
+
*getRestingLimitAsks(
|
|
925
|
+
marketIndex: number,
|
|
926
|
+
slot: number,
|
|
927
|
+
marketType: MarketType,
|
|
928
|
+
oraclePriceData: OraclePriceData,
|
|
929
|
+
minPerpAuctionDuration: number
|
|
930
|
+
): Generator<DLOBNode> {
|
|
931
|
+
for (const node of this.getLimitAsks(
|
|
932
|
+
marketIndex,
|
|
933
|
+
slot,
|
|
934
|
+
marketType,
|
|
935
|
+
oraclePriceData
|
|
936
|
+
)) {
|
|
937
|
+
if (this.isRestingLimitOrder(node.order, slot, minPerpAuctionDuration)) {
|
|
938
|
+
yield node;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
isRestingLimitOrder(
|
|
944
|
+
order: 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
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
918
954
|
*getLimitBids(
|
|
919
955
|
marketIndex: number,
|
|
920
956
|
slot: number,
|
|
@@ -947,6 +983,31 @@ export class DLOB {
|
|
|
947
983
|
);
|
|
948
984
|
}
|
|
949
985
|
|
|
986
|
+
/**
|
|
987
|
+
* Filters the limit bids that are post only or have been place for sufficiently long
|
|
988
|
+
* Useful for displaying order book that doesn't have taker limit orders crossing spread
|
|
989
|
+
*
|
|
990
|
+
* @returns
|
|
991
|
+
*/
|
|
992
|
+
*getRestingLimitBids(
|
|
993
|
+
marketIndex: number,
|
|
994
|
+
slot: number,
|
|
995
|
+
marketType: MarketType,
|
|
996
|
+
oraclePriceData: OraclePriceData,
|
|
997
|
+
minPerpAuctionDuration: number
|
|
998
|
+
): Generator<DLOBNode> {
|
|
999
|
+
for (const node of this.getLimitBids(
|
|
1000
|
+
marketIndex,
|
|
1001
|
+
slot,
|
|
1002
|
+
marketType,
|
|
1003
|
+
oraclePriceData
|
|
1004
|
+
)) {
|
|
1005
|
+
if (this.isRestingLimitOrder(node.order, slot, minPerpAuctionDuration)) {
|
|
1006
|
+
yield node;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
950
1011
|
*getAsks(
|
|
951
1012
|
marketIndex: number,
|
|
952
1013
|
fallbackAsk: BN | undefined,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { AnchorProvider, BN } from '@project-serum/anchor';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BASE_PRECISION,
|
|
4
|
+
calculateBidAskPrice,
|
|
5
|
+
getMarketOrderParams,
|
|
6
|
+
Wallet,
|
|
7
|
+
} from '..';
|
|
3
8
|
import { Token, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
4
9
|
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
5
10
|
import {
|
|
6
|
-
calculateReservePrice,
|
|
7
11
|
DriftClient,
|
|
8
12
|
User,
|
|
9
13
|
initialize,
|
|
@@ -11,7 +15,7 @@ import {
|
|
|
11
15
|
convertToNumber,
|
|
12
16
|
calculateTradeSlippage,
|
|
13
17
|
BulkAccountLoader,
|
|
14
|
-
|
|
18
|
+
getMarketsAndOraclesForSubscription,
|
|
15
19
|
PRICE_PRECISION,
|
|
16
20
|
QUOTE_PRECISION,
|
|
17
21
|
} from '..';
|
|
@@ -29,11 +33,11 @@ export const getTokenAddress = (
|
|
|
29
33
|
);
|
|
30
34
|
};
|
|
31
35
|
|
|
32
|
-
const
|
|
36
|
+
const env = 'devnet';
|
|
33
37
|
|
|
34
38
|
const main = async () => {
|
|
35
39
|
// Initialize Drift SDK
|
|
36
|
-
const sdkConfig = initialize({ env
|
|
40
|
+
const sdkConfig = initialize({ env });
|
|
37
41
|
|
|
38
42
|
// Set up the Wallet and Provider
|
|
39
43
|
const privateKey = process.env.BOT_PRIVATE_KEY; // stored as an array string
|
|
@@ -74,10 +78,7 @@ const main = async () => {
|
|
|
74
78
|
connection,
|
|
75
79
|
wallet: provider.wallet,
|
|
76
80
|
programID: driftPublicKey,
|
|
77
|
-
|
|
78
|
-
spotMarketIndexes: SpotMarkets[cluster].map(
|
|
79
|
-
(spotMarket) => spotMarket.marketIndex
|
|
80
|
-
),
|
|
81
|
+
...getMarketsAndOraclesForSubscription(env),
|
|
81
82
|
accountSubscription: {
|
|
82
83
|
type: 'polling',
|
|
83
84
|
accountLoader: bulkAccountLoader,
|
|
@@ -85,7 +86,7 @@ const main = async () => {
|
|
|
85
86
|
});
|
|
86
87
|
await driftClient.subscribe();
|
|
87
88
|
|
|
88
|
-
// Set up
|
|
89
|
+
// Set up user client
|
|
89
90
|
const user = new User({
|
|
90
91
|
driftClient: driftClient,
|
|
91
92
|
userAccountPublicKey: await driftClient.getUserAccountPublicKey(),
|
|
@@ -95,7 +96,7 @@ const main = async () => {
|
|
|
95
96
|
},
|
|
96
97
|
});
|
|
97
98
|
|
|
98
|
-
//// Check if
|
|
99
|
+
//// Check if user account exists for the current wallet
|
|
99
100
|
const userAccountExists = await user.exists();
|
|
100
101
|
|
|
101
102
|
if (!userAccountExists) {
|
|
@@ -118,54 +119,45 @@ const main = async () => {
|
|
|
118
119
|
(market) => market.baseAssetSymbol === 'SOL'
|
|
119
120
|
);
|
|
120
121
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const marketIndex = solMarketInfo.marketIndex;
|
|
123
|
+
const [bid, ask] = calculateBidAskPrice(
|
|
124
|
+
driftClient.getPerpMarketAccount(marketIndex).amm,
|
|
125
|
+
driftClient.getOracleDataForPerpMarket(marketIndex)
|
|
124
126
|
);
|
|
125
127
|
|
|
126
|
-
const
|
|
128
|
+
const formattedBidPrice = convertToNumber(bid, PRICE_PRECISION);
|
|
129
|
+
const formattedAskPrice = convertToNumber(ask, PRICE_PRECISION);
|
|
127
130
|
|
|
128
|
-
console.log(
|
|
131
|
+
console.log(
|
|
132
|
+
`Current amm bid and ask price are $${formattedBidPrice} and $${formattedAskPrice}`
|
|
133
|
+
);
|
|
129
134
|
|
|
130
135
|
// Estimate the slippage for a $5000 LONG trade
|
|
131
136
|
const solMarketAccount = driftClient.getPerpMarketAccount(
|
|
132
137
|
solMarketInfo.marketIndex
|
|
133
138
|
);
|
|
134
139
|
|
|
135
|
-
const longAmount = new BN(5000).mul(QUOTE_PRECISION);
|
|
136
140
|
const slippage = convertToNumber(
|
|
137
141
|
calculateTradeSlippage(
|
|
138
142
|
PositionDirection.LONG,
|
|
139
|
-
|
|
143
|
+
new BN(1).mul(BASE_PRECISION),
|
|
140
144
|
solMarketAccount,
|
|
141
|
-
'
|
|
142
|
-
|
|
145
|
+
'base',
|
|
146
|
+
driftClient.getOracleDataForPerpMarket(solMarketInfo.marketIndex)
|
|
143
147
|
)[0],
|
|
144
148
|
PRICE_PRECISION
|
|
145
149
|
);
|
|
146
150
|
|
|
147
|
-
console.log(
|
|
148
|
-
`Slippage for a $5000 LONG on the SOL market would be $${slippage}`
|
|
149
|
-
);
|
|
151
|
+
console.log(`Slippage for a 1 SOL-PERP would be $${slippage}`);
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
await driftClient.placePerpOrder(
|
|
154
|
+
getMarketOrderParams({
|
|
155
|
+
baseAssetAmount: new BN(1).mul(BASE_PRECISION),
|
|
156
|
+
direction: PositionDirection.LONG,
|
|
157
|
+
marketIndex: solMarketAccount.marketIndex,
|
|
158
|
+
})
|
|
156
159
|
);
|
|
157
|
-
console.log(`
|
|
158
|
-
|
|
159
|
-
// Reduce the position by $2000
|
|
160
|
-
const reduceAmount = new BN(2000).mul(QUOTE_PRECISION);
|
|
161
|
-
await driftClient.openPosition(
|
|
162
|
-
PositionDirection.SHORT,
|
|
163
|
-
reduceAmount,
|
|
164
|
-
solMarketInfo.marketIndex
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// Close the rest of the position
|
|
168
|
-
await driftClient.closePosition(solMarketInfo.marketIndex);
|
|
160
|
+
console.log(`Placed a 1 SOL-PERP LONG order`);
|
|
169
161
|
};
|
|
170
162
|
|
|
171
163
|
main();
|
package/src/idl/drift.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.12.0-beta.1",
|
|
3
3
|
"name": "drift",
|
|
4
4
|
"instructions": [
|
|
5
5
|
{
|
|
@@ -8568,6 +8568,11 @@
|
|
|
8568
8568
|
"code": 6224,
|
|
8569
8569
|
"name": "MaxNumberOfUsers",
|
|
8570
8570
|
"msg": "Max Number Of Users"
|
|
8571
|
+
},
|
|
8572
|
+
{
|
|
8573
|
+
"code": 6225,
|
|
8574
|
+
"name": "InvalidOracleForSettlePnl",
|
|
8575
|
+
"msg": "InvalidOracleForSettlePnl"
|
|
8571
8576
|
}
|
|
8572
8577
|
]
|
|
8573
8578
|
}
|
package/src/math/trade.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PerpMarketAccount, PositionDirection } from '../types';
|
|
1
|
+
import { MarketType, PerpMarketAccount, PositionDirection } from '../types';
|
|
2
2
|
import { BN } from '@project-serum/anchor';
|
|
3
3
|
import { assert } from '../assert/assert';
|
|
4
4
|
import {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
PEG_PRECISION,
|
|
7
7
|
AMM_TO_QUOTE_PRECISION_RATIO,
|
|
8
8
|
ZERO,
|
|
9
|
+
BASE_PRECISION,
|
|
9
10
|
} from '../constants/numericConstants';
|
|
10
11
|
import {
|
|
11
12
|
calculateBidPrice,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
import { squareRootBN } from './utils';
|
|
24
25
|
import { isVariant } from '../types';
|
|
25
26
|
import { OraclePriceData } from '../oracles/types';
|
|
27
|
+
import { DLOB } from '../dlob/DLOB';
|
|
26
28
|
|
|
27
29
|
const MAXPCT = new BN(1000); //percentage units are [0,1000] => [0,1]
|
|
28
30
|
|
|
@@ -61,7 +63,7 @@ export function calculateTradeSlippage(
|
|
|
61
63
|
amount: BN,
|
|
62
64
|
market: PerpMarketAccount,
|
|
63
65
|
inputAssetType: AssetType = 'quote',
|
|
64
|
-
oraclePriceData
|
|
66
|
+
oraclePriceData: OraclePriceData,
|
|
65
67
|
useSpread = true
|
|
66
68
|
): [BN, BN, BN, BN] {
|
|
67
69
|
let oldPrice: BN;
|
|
@@ -349,3 +351,216 @@ export function calculateTargetPriceTrade(
|
|
|
349
351
|
return [direction, baseSize, entryPrice, targetPrice];
|
|
350
352
|
}
|
|
351
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Calculates the estimated entry price and price impact of order, in base or quote
|
|
357
|
+
* Price impact is based on the difference between the entry price and the best bid/ask price (whether it's dlob or vamm)
|
|
358
|
+
*
|
|
359
|
+
* @param assetType
|
|
360
|
+
* @param amount
|
|
361
|
+
* @param direction
|
|
362
|
+
* @param market
|
|
363
|
+
* @param oraclePriceData
|
|
364
|
+
* @param dlob
|
|
365
|
+
* @param slot
|
|
366
|
+
* @param minPerpAuctionDuration
|
|
367
|
+
*/
|
|
368
|
+
export function calculateEstimatedPerpEntryPrice(
|
|
369
|
+
assetType: AssetType,
|
|
370
|
+
amount: BN,
|
|
371
|
+
direction: PositionDirection,
|
|
372
|
+
market: PerpMarketAccount,
|
|
373
|
+
oraclePriceData: OraclePriceData,
|
|
374
|
+
dlob: DLOB,
|
|
375
|
+
slot: number,
|
|
376
|
+
minPerpAuctionDuration: number
|
|
377
|
+
): [BN, BN] {
|
|
378
|
+
if (amount.eq(ZERO)) {
|
|
379
|
+
return [ZERO, ZERO];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const takerIsLong = isVariant(direction, 'long');
|
|
383
|
+
const limitOrders = dlob[
|
|
384
|
+
takerIsLong ? 'getRestingLimitAsks' : 'getRestingLimitBids'
|
|
385
|
+
](
|
|
386
|
+
market.marketIndex,
|
|
387
|
+
slot,
|
|
388
|
+
MarketType.PERP,
|
|
389
|
+
oraclePriceData,
|
|
390
|
+
minPerpAuctionDuration
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const swapDirection = getSwapDirection(assetType, direction);
|
|
394
|
+
|
|
395
|
+
const { baseAssetReserve, quoteAssetReserve, sqrtK, newPeg } =
|
|
396
|
+
calculateUpdatedAMMSpreadReserves(market.amm, direction, oraclePriceData);
|
|
397
|
+
const amm = {
|
|
398
|
+
baseAssetReserve,
|
|
399
|
+
quoteAssetReserve,
|
|
400
|
+
sqrtK: sqrtK,
|
|
401
|
+
pegMultiplier: newPeg,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const invariant = amm.sqrtK.mul(amm.sqrtK);
|
|
405
|
+
|
|
406
|
+
let initialPrice = calculatePrice(
|
|
407
|
+
amm.baseAssetReserve,
|
|
408
|
+
amm.quoteAssetReserve,
|
|
409
|
+
amm.pegMultiplier
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
let cumulativeBaseFilled = ZERO;
|
|
413
|
+
let cumulativeQuoteFilled = ZERO;
|
|
414
|
+
|
|
415
|
+
let limitOrder = limitOrders.next().value;
|
|
416
|
+
if (limitOrder) {
|
|
417
|
+
const limitOrderPrice = limitOrder.getPrice(oraclePriceData, slot);
|
|
418
|
+
initialPrice = takerIsLong
|
|
419
|
+
? BN.min(limitOrderPrice, initialPrice)
|
|
420
|
+
: BN.max(limitOrderPrice, initialPrice);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (assetType === 'base') {
|
|
424
|
+
while (!cumulativeBaseFilled.eq(amount)) {
|
|
425
|
+
const limitOrderPrice = limitOrder?.getPrice(oraclePriceData, slot);
|
|
426
|
+
|
|
427
|
+
let maxAmmFill: BN;
|
|
428
|
+
if (limitOrderPrice) {
|
|
429
|
+
const newBaseReserves = squareRootBN(
|
|
430
|
+
invariant
|
|
431
|
+
.mul(PRICE_PRECISION)
|
|
432
|
+
.mul(amm.pegMultiplier)
|
|
433
|
+
.div(limitOrderPrice)
|
|
434
|
+
.div(PEG_PRECISION)
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// will be zero if the limit order price is better than the amm price
|
|
438
|
+
maxAmmFill = takerIsLong
|
|
439
|
+
? amm.baseAssetReserve.sub(newBaseReserves)
|
|
440
|
+
: newBaseReserves.sub(amm.baseAssetReserve);
|
|
441
|
+
} else {
|
|
442
|
+
maxAmmFill = amount.sub(cumulativeBaseFilled);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (maxAmmFill.gt(ZERO)) {
|
|
446
|
+
const baseFilled = BN.min(amount.sub(cumulativeBaseFilled), maxAmmFill);
|
|
447
|
+
const [afterSwapQuoteReserves, afterSwapBaseReserves] =
|
|
448
|
+
calculateAmmReservesAfterSwap(amm, 'base', baseFilled, swapDirection);
|
|
449
|
+
|
|
450
|
+
const quoteFilled = calculateQuoteAssetAmountSwapped(
|
|
451
|
+
amm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(),
|
|
452
|
+
amm.pegMultiplier,
|
|
453
|
+
swapDirection
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
457
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
458
|
+
|
|
459
|
+
amm.baseAssetReserve = afterSwapBaseReserves;
|
|
460
|
+
amm.quoteAssetReserve = afterSwapQuoteReserves;
|
|
461
|
+
|
|
462
|
+
if (cumulativeBaseFilled.eq(amount)) {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const baseFilled = BN.min(
|
|
468
|
+
limitOrder.order.baseAssetAmount.sub(
|
|
469
|
+
limitOrder.order.baseAssetAmountFilled
|
|
470
|
+
),
|
|
471
|
+
amount.sub(cumulativeBaseFilled)
|
|
472
|
+
);
|
|
473
|
+
const quoteFilled = baseFilled.mul(limitOrderPrice).div(BASE_PRECISION);
|
|
474
|
+
|
|
475
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
476
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
477
|
+
|
|
478
|
+
if (cumulativeBaseFilled.eq(amount)) {
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
limitOrder = limitOrders.next().value;
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
while (!cumulativeQuoteFilled.eq(amount)) {
|
|
486
|
+
const limitOrderPrice = limitOrder?.getPrice(oraclePriceData, slot);
|
|
487
|
+
|
|
488
|
+
let maxAmmFill: BN;
|
|
489
|
+
if (limitOrderPrice) {
|
|
490
|
+
const newQuoteReserves = squareRootBN(
|
|
491
|
+
invariant
|
|
492
|
+
.mul(PEG_PRECISION)
|
|
493
|
+
.mul(limitOrderPrice)
|
|
494
|
+
.div(amm.pegMultiplier)
|
|
495
|
+
.div(PRICE_PRECISION)
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// will be zero if the limit order price is better than the amm price
|
|
499
|
+
maxAmmFill = takerIsLong
|
|
500
|
+
? newQuoteReserves.sub(amm.quoteAssetReserve)
|
|
501
|
+
: amm.quoteAssetReserve.sub(newQuoteReserves);
|
|
502
|
+
} else {
|
|
503
|
+
maxAmmFill = amount.sub(cumulativeQuoteFilled);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (maxAmmFill.gt(ZERO)) {
|
|
507
|
+
const quoteFilled = BN.min(
|
|
508
|
+
amount.sub(cumulativeQuoteFilled),
|
|
509
|
+
maxAmmFill
|
|
510
|
+
);
|
|
511
|
+
const [afterSwapQuoteReserves, afterSwapBaseReserves] =
|
|
512
|
+
calculateAmmReservesAfterSwap(
|
|
513
|
+
amm,
|
|
514
|
+
'quote',
|
|
515
|
+
quoteFilled,
|
|
516
|
+
swapDirection
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const baseFilled = afterSwapBaseReserves
|
|
520
|
+
.sub(amm.baseAssetReserve)
|
|
521
|
+
.abs();
|
|
522
|
+
|
|
523
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
524
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
525
|
+
|
|
526
|
+
amm.baseAssetReserve = afterSwapBaseReserves;
|
|
527
|
+
amm.quoteAssetReserve = afterSwapQuoteReserves;
|
|
528
|
+
|
|
529
|
+
if (cumulativeQuoteFilled.eq(amount)) {
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const quoteFilled = BN.min(
|
|
535
|
+
limitOrder.order.baseAssetAmount
|
|
536
|
+
.sub(limitOrder.order.baseAssetAmountFilled)
|
|
537
|
+
.mul(limitOrderPrice)
|
|
538
|
+
.div(BASE_PRECISION),
|
|
539
|
+
amount.sub(cumulativeQuoteFilled)
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const baseFilled = quoteFilled.mul(BASE_PRECISION).div(limitOrderPrice);
|
|
543
|
+
|
|
544
|
+
cumulativeBaseFilled = cumulativeBaseFilled.add(baseFilled);
|
|
545
|
+
cumulativeQuoteFilled = cumulativeQuoteFilled.add(quoteFilled);
|
|
546
|
+
|
|
547
|
+
if (cumulativeQuoteFilled.eq(amount)) {
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
limitOrder = limitOrders.next().value;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const entryPrice = cumulativeQuoteFilled
|
|
556
|
+
.mul(BASE_PRECISION)
|
|
557
|
+
.div(cumulativeBaseFilled);
|
|
558
|
+
|
|
559
|
+
const priceImpact = entryPrice
|
|
560
|
+
.sub(initialPrice)
|
|
561
|
+
.mul(PRICE_PRECISION)
|
|
562
|
+
.div(initialPrice)
|
|
563
|
+
.abs();
|
|
564
|
+
|
|
565
|
+
return [entryPrice, priceImpact];
|
|
566
|
+
}
|