@drift-labs/sdk 0.2.0-master.25 → 0.2.0-master.27

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.
Files changed (124) hide show
  1. package/lib/accounts/pollingClearingHouseAccountSubscriber.d.ts +14 -13
  2. package/lib/accounts/pollingClearingHouseAccountSubscriber.js +30 -27
  3. package/lib/accounts/types.d.ts +9 -9
  4. package/lib/accounts/webSocketClearingHouseAccountSubscriber.d.ts +15 -14
  5. package/lib/accounts/webSocketClearingHouseAccountSubscriber.js +38 -34
  6. package/lib/addresses/pda.d.ts +7 -6
  7. package/lib/addresses/pda.js +31 -27
  8. package/lib/admin.d.ts +13 -7
  9. package/lib/admin.js +111 -44
  10. package/lib/clearingHouse.d.ts +71 -42
  11. package/lib/clearingHouse.js +767 -278
  12. package/lib/clearingHouseConfig.d.ts +2 -2
  13. package/lib/clearingHouseUser.d.ts +24 -22
  14. package/lib/clearingHouseUser.js +273 -177
  15. package/lib/config.d.ts +7 -7
  16. package/lib/config.js +21 -21
  17. package/lib/constants/numericConstants.d.ts +12 -12
  18. package/lib/constants/numericConstants.js +13 -13
  19. package/lib/constants/{markets.d.ts → perpMarkets.d.ts} +5 -5
  20. package/lib/constants/{markets.js → perpMarkets.js} +4 -4
  21. package/lib/constants/{banks.d.ts → spotMarkets.d.ts} +6 -6
  22. package/lib/constants/{banks.js → spotMarkets.js} +16 -16
  23. package/lib/dlob/DLOB.d.ts +73 -0
  24. package/lib/dlob/DLOB.js +557 -0
  25. package/lib/dlob/DLOBNode.d.ts +52 -0
  26. package/lib/dlob/DLOBNode.js +82 -0
  27. package/lib/dlob/NodeList.d.ts +26 -0
  28. package/lib/dlob/NodeList.js +138 -0
  29. package/lib/events/types.d.ts +2 -1
  30. package/lib/events/types.js +1 -0
  31. package/lib/examples/makeTradeExample.js +7 -7
  32. package/lib/idl/clearing_house.json +1152 -503
  33. package/lib/index.d.ts +10 -3
  34. package/lib/index.js +10 -3
  35. package/lib/math/amm.d.ts +2 -2
  36. package/lib/math/amm.js +1 -1
  37. package/lib/math/funding.d.ts +6 -6
  38. package/lib/math/funding.js +2 -1
  39. package/lib/math/margin.d.ts +4 -4
  40. package/lib/math/margin.js +18 -11
  41. package/lib/math/market.d.ts +11 -10
  42. package/lib/math/market.js +30 -7
  43. package/lib/math/oracles.d.ts +2 -1
  44. package/lib/math/oracles.js +11 -1
  45. package/lib/math/orders.d.ts +6 -6
  46. package/lib/math/orders.js +31 -16
  47. package/lib/math/position.d.ts +13 -13
  48. package/lib/math/position.js +19 -19
  49. package/lib/math/spotBalance.d.ts +22 -0
  50. package/lib/math/spotBalance.js +193 -0
  51. package/lib/math/spotMarket.d.ts +4 -0
  52. package/lib/math/spotMarket.js +8 -0
  53. package/lib/math/spotPosition.d.ts +6 -0
  54. package/lib/math/spotPosition.js +23 -0
  55. package/lib/math/state.js +2 -2
  56. package/lib/math/trade.d.ts +4 -4
  57. package/lib/orderParams.d.ts +4 -4
  58. package/lib/orderParams.js +12 -4
  59. package/lib/serum/serumSubscriber.d.ts +23 -0
  60. package/lib/serum/serumSubscriber.js +41 -0
  61. package/lib/serum/types.d.ts +11 -0
  62. package/lib/serum/types.js +2 -0
  63. package/lib/tx/retryTxSender.d.ts +1 -1
  64. package/lib/tx/retryTxSender.js +4 -2
  65. package/lib/tx/types.d.ts +1 -1
  66. package/lib/types.d.ts +148 -57
  67. package/lib/types.js +39 -11
  68. package/lib/userMap/userMap.d.ts +25 -0
  69. package/lib/userMap/userMap.js +73 -0
  70. package/lib/userMap/userStatsMap.d.ts +19 -0
  71. package/lib/userMap/userStatsMap.js +68 -0
  72. package/package.json +6 -3
  73. package/src/accounts/pollingClearingHouseAccountSubscriber.ts +42 -38
  74. package/src/accounts/types.ts +12 -9
  75. package/src/accounts/webSocketClearingHouseAccountSubscriber.ts +65 -52
  76. package/src/addresses/pda.ts +49 -44
  77. package/src/admin.ts +190 -55
  78. package/src/clearingHouse.ts +1092 -365
  79. package/src/clearingHouseConfig.ts +2 -2
  80. package/src/clearingHouseUser.ts +518 -255
  81. package/src/config.ts +30 -30
  82. package/src/constants/numericConstants.ts +17 -15
  83. package/src/constants/{markets.ts → perpMarkets.ts} +5 -5
  84. package/src/constants/{banks.ts → spotMarkets.ts} +19 -19
  85. package/src/dlob/DLOB.ts +884 -0
  86. package/src/dlob/DLOBNode.ts +163 -0
  87. package/src/dlob/NodeList.ts +185 -0
  88. package/src/events/types.ts +3 -0
  89. package/src/examples/makeTradeExample.js +152 -75
  90. package/src/examples/makeTradeExample.ts +10 -8
  91. package/src/idl/clearing_house.json +1152 -503
  92. package/src/index.ts +10 -3
  93. package/src/math/amm.ts +6 -3
  94. package/src/math/funding.ts +7 -7
  95. package/src/math/margin.ts +34 -23
  96. package/src/math/market.ts +72 -20
  97. package/src/math/oracles.ts +18 -1
  98. package/src/math/orders.ts +33 -25
  99. package/src/math/position.ts +31 -31
  100. package/src/math/spotBalance.ts +316 -0
  101. package/src/math/spotMarket.ts +9 -0
  102. package/src/math/spotPosition.ts +47 -0
  103. package/src/math/state.ts +2 -2
  104. package/src/math/trade.ts +4 -4
  105. package/src/orderParams.ts +16 -8
  106. package/src/serum/serumSubscriber.ts +80 -0
  107. package/src/serum/types.ts +13 -0
  108. package/src/tx/retryTxSender.ts +5 -2
  109. package/src/tx/types.ts +2 -1
  110. package/src/types.ts +135 -56
  111. package/src/userMap/userMap.ts +100 -0
  112. package/src/userMap/userStatsMap.ts +110 -0
  113. package/tests/bn/test.ts +2 -3
  114. package/tests/dlob/helpers.ts +322 -0
  115. package/tests/dlob/test.ts +2865 -0
  116. package/lib/math/bankBalance.d.ts +0 -15
  117. package/lib/math/bankBalance.js +0 -150
  118. package/src/constants/numericConstants.js +0 -41
  119. package/src/math/bankBalance.ts +0 -258
  120. package/src/math/oracles.js +0 -26
  121. package/src/math/state.js +0 -15
  122. package/src/orderParams.js +0 -20
  123. package/src/slot/SlotSubscriber.js +0 -39
  124. package/src/tokenFaucet.js +0 -189
@@ -0,0 +1,884 @@
1
+ import { getOrderSignature, getVammNodeGenerator, NodeList } from './NodeList';
2
+ import {
3
+ MarketType,
4
+ BN,
5
+ calculateAskPrice,
6
+ calculateBidPrice,
7
+ ClearingHouse,
8
+ convertToNumber,
9
+ isAuctionComplete,
10
+ isOrderExpired,
11
+ isOneOfVariant,
12
+ isVariant,
13
+ getVariant,
14
+ Order,
15
+ ZERO,
16
+ MARK_PRICE_PRECISION,
17
+ SpotMarketAccount,
18
+ PerpMarketAccount,
19
+ OraclePriceData,
20
+ SlotSubscriber,
21
+ UserMap,
22
+ MarketTypeStr,
23
+ } from '..';
24
+ import { PublicKey } from '@solana/web3.js';
25
+ import { DLOBNode, DLOBNodeType, TriggerOrderNode } from '..';
26
+
27
+ export type MarketNodeLists = {
28
+ limit: {
29
+ ask: NodeList<'limit'>;
30
+ bid: NodeList<'limit'>;
31
+ };
32
+ floatingLimit: {
33
+ ask: NodeList<'floatingLimit'>;
34
+ bid: NodeList<'floatingLimit'>;
35
+ };
36
+ market: {
37
+ ask: NodeList<'market'>;
38
+ bid: NodeList<'market'>;
39
+ };
40
+ trigger: {
41
+ above: NodeList<'trigger'>;
42
+ below: NodeList<'trigger'>;
43
+ };
44
+ };
45
+
46
+ type OrderBookCallback = () => void;
47
+
48
+ export type NodeToFill = {
49
+ node: DLOBNode;
50
+ makerNode?: DLOBNode;
51
+ };
52
+
53
+ export type NodeToTrigger = {
54
+ node: TriggerOrderNode;
55
+ };
56
+
57
+ type Side = 'ask' | 'bid' | 'both';
58
+
59
+ export class DLOB {
60
+ openOrders = new Map<MarketTypeStr, Set<string>>();
61
+ orderLists = new Map<MarketTypeStr, Map<number, MarketNodeLists>>();
62
+ marketIndexToAccount = new Map<
63
+ MarketTypeStr,
64
+ Map<number, PerpMarketAccount | SpotMarketAccount>
65
+ >();
66
+
67
+ silent = false;
68
+ initialized = false;
69
+
70
+ /**
71
+ *
72
+ * @param perpMarkets The perp markets to maintain a DLOB for
73
+ * @param spotMarkets The spot markets to maintain a DLOB for
74
+ * @param silent set to true to prevent logging on inserts and removals
75
+ */
76
+ public constructor(
77
+ perpMarkets: PerpMarketAccount[],
78
+ spotMarkets: SpotMarketAccount[],
79
+ silent?: boolean
80
+ ) {
81
+ this.silent = silent;
82
+
83
+ this.openOrders.set('perp', new Set<string>());
84
+ this.openOrders.set('spot', new Set<string>());
85
+ this.orderLists.set('perp', new Map<number, MarketNodeLists>());
86
+ this.orderLists.set('spot', new Map<number, MarketNodeLists>());
87
+ this.marketIndexToAccount.set('perp', new Map<number, PerpMarketAccount>());
88
+ this.marketIndexToAccount.set('spot', new Map<number, SpotMarketAccount>());
89
+
90
+ for (const market of perpMarkets) {
91
+ const marketIndex = market.marketIndex;
92
+ this.marketIndexToAccount.get('perp').set(marketIndex.toNumber(), market);
93
+
94
+ this.orderLists.get('perp').set(marketIndex.toNumber(), {
95
+ limit: {
96
+ ask: new NodeList('limit', 'asc'),
97
+ bid: new NodeList('limit', 'desc'),
98
+ },
99
+ floatingLimit: {
100
+ ask: new NodeList('floatingLimit', 'asc'),
101
+ bid: new NodeList('floatingLimit', 'desc'),
102
+ },
103
+ market: {
104
+ ask: new NodeList('market', 'asc'),
105
+ bid: new NodeList('market', 'asc'), // always sort ascending for market orders
106
+ },
107
+ trigger: {
108
+ above: new NodeList('trigger', 'asc'),
109
+ below: new NodeList('trigger', 'desc'),
110
+ },
111
+ });
112
+ }
113
+ for (const market of spotMarkets) {
114
+ const marketIndex = market.marketIndex;
115
+ this.marketIndexToAccount.get('spot').set(marketIndex.toNumber(), market);
116
+
117
+ this.orderLists.get('spot').set(marketIndex.toNumber(), {
118
+ limit: {
119
+ ask: new NodeList('limit', 'asc'),
120
+ bid: new NodeList('limit', 'desc'),
121
+ },
122
+ floatingLimit: {
123
+ ask: new NodeList('floatingLimit', 'asc'),
124
+ bid: new NodeList('floatingLimit', 'desc'),
125
+ },
126
+ market: {
127
+ ask: new NodeList('market', 'asc'),
128
+ bid: new NodeList('market', 'asc'), // always sort ascending for market orders
129
+ },
130
+ trigger: {
131
+ above: new NodeList('trigger', 'asc'),
132
+ below: new NodeList('trigger', 'desc'),
133
+ },
134
+ });
135
+ }
136
+ }
137
+
138
+ /**
139
+ * initializes a new DLOB instance
140
+ *
141
+ * @param clearingHouse The ClearingHouse instance to use for price data
142
+ * @returns a promise that resolves when the DLOB is initialized
143
+ */
144
+ public async init(
145
+ clearingHouse: ClearingHouse,
146
+ userMap?: UserMap
147
+ ): Promise<boolean> {
148
+ if (this.initialized) {
149
+ return false;
150
+ }
151
+ if (userMap) {
152
+ // initialize the dlob with the user map (prevents hitting getProgramAccounts)
153
+ for (const user of userMap.values()) {
154
+ const userAccount = user.getUserAccount();
155
+ const userAccountPubkey = user.getUserAccountPublicKey();
156
+
157
+ for (const order of userAccount.orders) {
158
+ this.insertOrder(order, userAccountPubkey);
159
+ }
160
+ }
161
+ } else {
162
+ const programAccounts = await clearingHouse.program.account.user.all();
163
+ for (const programAccount of programAccounts) {
164
+ // @ts-ignore
165
+ const userAccount: UserAccount = programAccount.account;
166
+ const userAccountPublicKey = programAccount.publicKey;
167
+
168
+ for (const order of userAccount.orders) {
169
+ this.insertOrder(order, userAccountPublicKey);
170
+ }
171
+ }
172
+ }
173
+
174
+ this.initialized = true;
175
+ return true;
176
+ }
177
+
178
+ public insertOrder(
179
+ order: Order,
180
+ userAccount: PublicKey,
181
+ onInsert?: OrderBookCallback
182
+ ): void {
183
+ if (isVariant(order.status, 'init')) {
184
+ return;
185
+ }
186
+
187
+ const marketType = getVariant(order.marketType) as MarketTypeStr;
188
+
189
+ if (isVariant(order.status, 'open')) {
190
+ this.openOrders
191
+ .get(marketType)
192
+ .add(getOrderSignature(order.orderId, userAccount));
193
+ }
194
+ this.getListForOrder(order).insert(
195
+ order,
196
+ marketType,
197
+ this.marketIndexToAccount
198
+ .get(marketType)
199
+ .get(order.marketIndex.toNumber()),
200
+ userAccount
201
+ );
202
+
203
+ if (onInsert) {
204
+ onInsert();
205
+ }
206
+ }
207
+
208
+ public trigger(
209
+ order: Order,
210
+ userAccount: PublicKey,
211
+ onTrigger?: OrderBookCallback
212
+ ): void {
213
+ if (isVariant(order.status, 'init')) {
214
+ return;
215
+ }
216
+
217
+ const marketType = getVariant(order.marketType) as MarketTypeStr;
218
+
219
+ const triggerList = this.orderLists
220
+ .get(marketType)
221
+ .get(order.marketIndex.toNumber()).trigger[
222
+ isVariant(order.triggerCondition, 'above') ? 'above' : 'below'
223
+ ];
224
+ triggerList.remove(order, userAccount);
225
+
226
+ this.getListForOrder(order).insert(
227
+ order,
228
+ marketType,
229
+ this.marketIndexToAccount
230
+ .get(marketType)
231
+ .get(order.marketIndex.toNumber()),
232
+ userAccount
233
+ );
234
+ if (onTrigger) {
235
+ onTrigger();
236
+ }
237
+ }
238
+
239
+ public getListForOrder(order: Order): NodeList<any> {
240
+ const isInactiveTriggerOrder =
241
+ isOneOfVariant(order.orderType, ['triggerMarket', 'triggerLimit']) &&
242
+ !order.triggered;
243
+
244
+ let type: DLOBNodeType;
245
+ if (isInactiveTriggerOrder) {
246
+ type = 'trigger';
247
+ } else if (isOneOfVariant(order.orderType, ['market', 'triggerMarket'])) {
248
+ type = 'market';
249
+ } else if (order.oraclePriceOffset.gt(ZERO)) {
250
+ type = 'floatingLimit';
251
+ } else {
252
+ type = 'limit';
253
+ }
254
+
255
+ let subType: string;
256
+ if (isInactiveTriggerOrder) {
257
+ subType = isVariant(order.triggerCondition, 'above') ? 'above' : 'below';
258
+ } else {
259
+ subType = isVariant(order.direction, 'long') ? 'bid' : 'ask';
260
+ }
261
+
262
+ const marketType = getVariant(order.marketType) as MarketTypeStr;
263
+ return this.orderLists.get(marketType).get(order.marketIndex.toNumber())[
264
+ type
265
+ ][subType];
266
+ }
267
+
268
+ public findNodesToFill(
269
+ marketIndex: BN,
270
+ vBid: BN | undefined,
271
+ vAsk: BN | undefined,
272
+ slot: number,
273
+ marketType: MarketType,
274
+ oraclePriceData: OraclePriceData
275
+ ): NodeToFill[] {
276
+ // Find all the crossing nodes
277
+ const crossingNodesToFill: Array<NodeToFill> = this.findCrossingNodesToFill(
278
+ marketIndex,
279
+ vBid,
280
+ vAsk,
281
+ slot,
282
+ marketType,
283
+ oraclePriceData
284
+ );
285
+
286
+ // get expired market nodes
287
+ const marketNodesToFill = this.findMarketNodesToFill(
288
+ marketIndex,
289
+ slot,
290
+ marketType
291
+ );
292
+ return crossingNodesToFill.concat(marketNodesToFill);
293
+ }
294
+
295
+ public findCrossingNodesToFill(
296
+ marketIndex: BN,
297
+ vBid: BN | undefined,
298
+ vAsk: BN | undefined,
299
+ slot: number,
300
+ marketType: MarketType,
301
+ oraclePriceData: OraclePriceData
302
+ ): NodeToFill[] {
303
+ const nodesToFill = new Array<NodeToFill>();
304
+
305
+ const askGenerator = this.getAsks(
306
+ marketIndex,
307
+ vAsk,
308
+ slot,
309
+ marketType,
310
+ oraclePriceData
311
+ );
312
+ const bidGenerator = this.getBids(
313
+ marketIndex,
314
+ vBid,
315
+ slot,
316
+ marketType,
317
+ oraclePriceData
318
+ );
319
+
320
+ let nextAsk = askGenerator.next();
321
+ let nextBid = bidGenerator.next();
322
+
323
+ // First try to find orders that cross
324
+ while (!nextAsk.done && !nextBid.done) {
325
+ const { crossingNodes, exhaustedSide } = this.findCrossingOrders(
326
+ nextAsk.value,
327
+ nextBid.value,
328
+ oraclePriceData,
329
+ slot
330
+ );
331
+
332
+ if (exhaustedSide === 'bid') {
333
+ nextBid = bidGenerator.next();
334
+ } else if (exhaustedSide === 'ask') {
335
+ nextAsk = askGenerator.next();
336
+ } else if (exhaustedSide === 'both') {
337
+ nextBid = bidGenerator.next();
338
+ nextAsk = askGenerator.next();
339
+ } else {
340
+ console.error(`invalid exhaustedSide: ${exhaustedSide}`);
341
+ break;
342
+ }
343
+
344
+ const takerIsMaker =
345
+ crossingNodes?.makerNode !== undefined &&
346
+ crossingNodes.node.userAccount.equals(
347
+ crossingNodes.makerNode.userAccount
348
+ );
349
+
350
+ // Verify that each side is different user
351
+ if (crossingNodes && !takerIsMaker) {
352
+ nodesToFill.push(crossingNodes);
353
+ }
354
+ }
355
+ return nodesToFill;
356
+ }
357
+
358
+ public findMarketNodesToFill(
359
+ marketIndex: BN,
360
+ slot: number,
361
+ marketType: MarketType
362
+ ): NodeToFill[] {
363
+ const nodesToFill = new Array<NodeToFill>();
364
+ // Then see if there are orders to fill against vamm
365
+ for (const marketBid of this.getMarketBids(marketIndex, marketType)) {
366
+ if (
367
+ isAuctionComplete(marketBid.order, slot) ||
368
+ isOrderExpired(marketBid.order, slot)
369
+ ) {
370
+ nodesToFill.push({
371
+ node: marketBid,
372
+ });
373
+ }
374
+ }
375
+
376
+ for (const marketAsk of this.getMarketAsks(marketIndex, marketType)) {
377
+ if (
378
+ isAuctionComplete(marketAsk.order, slot) ||
379
+ isOrderExpired(marketAsk.order, slot)
380
+ ) {
381
+ nodesToFill.push({
382
+ node: marketAsk,
383
+ });
384
+ }
385
+ }
386
+ return nodesToFill;
387
+ }
388
+
389
+ public findJitAuctionNodesToFill(
390
+ marketIndex: BN,
391
+ slot: number,
392
+ marketType: MarketType
393
+ ): NodeToFill[] {
394
+ const nodesToFill = new Array<NodeToFill>();
395
+ // Then see if there are orders still in JIT auction
396
+ for (const marketBid of this.getMarketBids(marketIndex, marketType)) {
397
+ if (!isAuctionComplete(marketBid.order, slot)) {
398
+ nodesToFill.push({
399
+ node: marketBid,
400
+ });
401
+ }
402
+ }
403
+
404
+ for (const marketAsk of this.getMarketAsks(marketIndex, marketType)) {
405
+ if (!isAuctionComplete(marketAsk.order, slot)) {
406
+ nodesToFill.push({
407
+ node: marketAsk,
408
+ });
409
+ }
410
+ }
411
+ return nodesToFill;
412
+ }
413
+
414
+ public getMarketBids(
415
+ marketIndex: BN,
416
+ marketType: MarketType
417
+ ): Generator<DLOBNode> {
418
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
419
+ return this.orderLists
420
+ .get(marketTypeStr)
421
+ .get(marketIndex.toNumber())
422
+ .market.bid.getGenerator();
423
+ }
424
+
425
+ public getMarketAsks(
426
+ marketIndex: BN,
427
+ marketType: MarketType
428
+ ): Generator<DLOBNode> {
429
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
430
+ return this.orderLists
431
+ .get(marketTypeStr)
432
+ .get(marketIndex.toNumber())
433
+ .market.ask.getGenerator();
434
+ }
435
+
436
+ *getAsks(
437
+ marketIndex: BN,
438
+ vAsk: BN | undefined,
439
+ slot: number,
440
+ marketType: MarketType,
441
+ oraclePriceData: OraclePriceData
442
+ ): Generator<DLOBNode> {
443
+ if (isVariant(marketType, 'spot') && !oraclePriceData) {
444
+ throw new Error('Must provide OraclePriceData to get spot asks');
445
+ }
446
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
447
+ const nodeLists = this.orderLists
448
+ .get(marketTypeStr)
449
+ .get(marketIndex.toNumber());
450
+
451
+ const generatorList = [
452
+ nodeLists.limit.ask.getGenerator(),
453
+ nodeLists.floatingLimit.ask.getGenerator(),
454
+ nodeLists.market.ask.getGenerator(),
455
+ ];
456
+
457
+ if (marketTypeStr === 'perp') {
458
+ generatorList.push(getVammNodeGenerator(vAsk));
459
+ }
460
+
461
+ const askGenerators = generatorList.map((generator) => {
462
+ return {
463
+ next: generator.next(),
464
+ generator,
465
+ };
466
+ });
467
+
468
+ let asksExhausted = false;
469
+ while (!asksExhausted) {
470
+ const bestGenerator = askGenerators.reduce(
471
+ (bestGenerator, currentGenerator) => {
472
+ if (currentGenerator.next.done) {
473
+ return bestGenerator;
474
+ }
475
+
476
+ if (bestGenerator.next.done) {
477
+ return currentGenerator;
478
+ }
479
+
480
+ const bestAskPrice = bestGenerator.next.value.getPrice(
481
+ oraclePriceData,
482
+ slot
483
+ );
484
+ const currentAskPrice = currentGenerator.next.value.getPrice(
485
+ oraclePriceData,
486
+ slot
487
+ );
488
+
489
+ return bestAskPrice.lt(currentAskPrice)
490
+ ? bestGenerator
491
+ : currentGenerator;
492
+ }
493
+ );
494
+
495
+ if (!bestGenerator.next.done) {
496
+ yield bestGenerator.next.value;
497
+ bestGenerator.next = bestGenerator.generator.next();
498
+ } else {
499
+ asksExhausted = true;
500
+ }
501
+ }
502
+ }
503
+
504
+ *getBids(
505
+ marketIndex: BN,
506
+ vBid: BN | undefined,
507
+ slot: number,
508
+ marketType: MarketType,
509
+ oraclePriceData: OraclePriceData
510
+ ): Generator<DLOBNode> {
511
+ if (isVariant(marketType, 'spot') && !oraclePriceData) {
512
+ throw new Error('Must provide OraclePriceData to get spot bids');
513
+ }
514
+
515
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
516
+ const nodeLists = this.orderLists
517
+ .get(marketTypeStr)
518
+ .get(marketIndex.toNumber());
519
+
520
+ const generatorList = [
521
+ nodeLists.limit.bid.getGenerator(),
522
+ nodeLists.floatingLimit.bid.getGenerator(),
523
+ nodeLists.market.bid.getGenerator(),
524
+ ];
525
+ if (marketTypeStr === 'perp') {
526
+ generatorList.push(getVammNodeGenerator(vBid));
527
+ }
528
+ const bidGenerators = generatorList.map((generator) => {
529
+ return {
530
+ next: generator.next(),
531
+ generator,
532
+ };
533
+ });
534
+
535
+ let bidsExhausted = false; // there will always be the vBid
536
+ while (!bidsExhausted) {
537
+ const bestGenerator = bidGenerators.reduce(
538
+ (bestGenerator, currentGenerator) => {
539
+ if (currentGenerator.next.done) {
540
+ return bestGenerator;
541
+ }
542
+
543
+ if (bestGenerator.next.done) {
544
+ return currentGenerator;
545
+ }
546
+
547
+ const bestBidPrice = bestGenerator.next.value.getPrice(
548
+ oraclePriceData,
549
+ slot
550
+ );
551
+ const currentBidPrice = currentGenerator.next.value.getPrice(
552
+ oraclePriceData,
553
+ slot
554
+ );
555
+
556
+ return bestBidPrice.gt(currentBidPrice)
557
+ ? bestGenerator
558
+ : currentGenerator;
559
+ }
560
+ );
561
+
562
+ if (!bestGenerator.next.done) {
563
+ yield bestGenerator.next.value;
564
+ bestGenerator.next = bestGenerator.generator.next();
565
+ } else {
566
+ bidsExhausted = true;
567
+ }
568
+ }
569
+ }
570
+
571
+ findCrossingOrders(
572
+ askNode: DLOBNode,
573
+ bidNode: DLOBNode,
574
+ oraclePriceData: OraclePriceData,
575
+ slot: number
576
+ ): {
577
+ crossingNodes?: NodeToFill;
578
+ exhaustedSide?: Side;
579
+ } {
580
+ const bidPrice = bidNode.getPrice(oraclePriceData, slot);
581
+ const askPrice = askNode.getPrice(oraclePriceData, slot);
582
+
583
+ const bidOrder = bidNode.order;
584
+ const askOrder = askNode.order;
585
+
586
+ const containsMarketOrder =
587
+ (askOrder && isVariant(askOrder.orderType, 'market')) ||
588
+ (bidOrder && isVariant(bidOrder.orderType, 'market'));
589
+
590
+ if (!containsMarketOrder || bidPrice.lt(askPrice)) {
591
+ // no market orders, and no crossing orders - return
592
+ if (askNode.isVammNode() && !bidNode.isVammNode()) {
593
+ return {
594
+ exhaustedSide: 'bid',
595
+ };
596
+ } else if (!askNode.isVammNode() && bidNode.isVammNode()) {
597
+ return {
598
+ exhaustedSide: 'ask',
599
+ };
600
+ } else {
601
+ return {
602
+ exhaustedSide: 'both',
603
+ };
604
+ }
605
+ }
606
+
607
+ // User bid crosses the vamm ask
608
+ if (askNode.isVammNode()) {
609
+ if (!isAuctionComplete(bidOrder, slot)) {
610
+ return {
611
+ exhaustedSide: 'bid',
612
+ };
613
+ }
614
+ return {
615
+ crossingNodes: {
616
+ node: bidNode,
617
+ },
618
+ exhaustedSide: 'bid',
619
+ };
620
+ }
621
+
622
+ // User ask crosses the vamm bid
623
+ if (bidNode.isVammNode()) {
624
+ if (!isAuctionComplete(askOrder, slot)) {
625
+ return {
626
+ exhaustedSide: 'ask',
627
+ };
628
+ }
629
+ return {
630
+ crossingNodes: {
631
+ node: askNode,
632
+ },
633
+ exhaustedSide: 'ask',
634
+ };
635
+ }
636
+
637
+ const bidBaseRemaining = bidOrder.baseAssetAmount.sub(
638
+ bidOrder.baseAssetAmountFilled
639
+ );
640
+ const askBaseRemaining = askOrder.baseAssetAmount.sub(
641
+ askOrder.baseAssetAmountFilled
642
+ );
643
+
644
+ let exhaustedSide: Side;
645
+ if (bidBaseRemaining.eq(askBaseRemaining)) {
646
+ exhaustedSide = 'both';
647
+ } else if (bidBaseRemaining.gt(askBaseRemaining)) {
648
+ exhaustedSide = 'ask';
649
+ } else {
650
+ exhaustedSide = 'bid';
651
+ }
652
+
653
+ // Can't match two maker orders
654
+ if (bidOrder.postOnly && askOrder.postOnly) {
655
+ return {
656
+ exhaustedSide,
657
+ };
658
+ }
659
+
660
+ // update the orders as if they fill - so we don't try to match them on the next iteration
661
+ if (exhaustedSide === 'ask') {
662
+ bidNode.order.baseAssetAmountFilled =
663
+ bidOrder.baseAssetAmountFilled.add(askBaseRemaining);
664
+ askNode.order.baseAssetAmountFilled = askOrder.baseAssetAmount;
665
+ } else if (exhaustedSide === 'bid') {
666
+ askNode.order.baseAssetAmountFilled =
667
+ askOrder.baseAssetAmountFilled.add(bidBaseRemaining);
668
+ bidNode.order.baseAssetAmountFilled = bidOrder.baseAssetAmount;
669
+ } else {
670
+ askNode.order.baseAssetAmountFilled = askOrder.baseAssetAmount;
671
+ bidNode.order.baseAssetAmountFilled = bidOrder.baseAssetAmount;
672
+ }
673
+
674
+ // Bid is maker
675
+ if (bidOrder.postOnly) {
676
+ return {
677
+ crossingNodes: {
678
+ node: askNode,
679
+ makerNode: bidNode,
680
+ },
681
+ exhaustedSide,
682
+ };
683
+ }
684
+
685
+ // Ask is maker
686
+ if (askOrder.postOnly) {
687
+ return {
688
+ crossingNodes: {
689
+ node: bidNode,
690
+ makerNode: askNode,
691
+ },
692
+ exhaustedSide,
693
+ };
694
+ }
695
+
696
+ // Both are takers
697
+ // older order is maker
698
+ const [olderNode, newerNode] = askOrder.ts.lt(bidOrder.ts)
699
+ ? [askNode, bidNode]
700
+ : [bidNode, askNode];
701
+ return {
702
+ crossingNodes: {
703
+ node: newerNode,
704
+ makerNode: olderNode,
705
+ },
706
+ exhaustedSide,
707
+ };
708
+ }
709
+
710
+ public getBestAsk(
711
+ marketIndex: BN,
712
+ vAsk: BN | undefined,
713
+ slot: number,
714
+ marketType: MarketType,
715
+ oraclePriceData: OraclePriceData
716
+ ): BN {
717
+ return this.getAsks(marketIndex, vAsk, slot, marketType, oraclePriceData)
718
+ .next()
719
+ .value.getPrice(oraclePriceData, slot);
720
+ }
721
+
722
+ public getBestBid(
723
+ marketIndex: BN,
724
+ vBid: BN | undefined,
725
+ slot: number,
726
+ marketType: MarketType,
727
+ oraclePriceData: OraclePriceData
728
+ ): BN {
729
+ return this.getBids(marketIndex, vBid, slot, marketType, oraclePriceData)
730
+ .next()
731
+ .value.getPrice(oraclePriceData, slot);
732
+ }
733
+
734
+ public findNodesToTrigger(
735
+ marketIndex: BN,
736
+ slot: number,
737
+ oraclePrice: BN,
738
+ marketType: MarketType
739
+ ): NodeToTrigger[] {
740
+ const nodesToTrigger = [];
741
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
742
+ for (const node of this.orderLists
743
+ .get(marketTypeStr)
744
+ .get(marketIndex.toNumber())
745
+ .trigger.above.getGenerator()) {
746
+ if (oraclePrice.gt(node.order.triggerPrice)) {
747
+ if (isAuctionComplete(node.order, slot)) {
748
+ nodesToTrigger.push({
749
+ node: node,
750
+ });
751
+ }
752
+ } else {
753
+ break;
754
+ }
755
+ }
756
+
757
+ for (const node of this.orderLists
758
+ .get(marketTypeStr)
759
+ .get(marketIndex.toNumber())
760
+ .trigger.below.getGenerator()) {
761
+ if (oraclePrice.lt(node.order.triggerPrice)) {
762
+ if (isAuctionComplete(node.order, slot)) {
763
+ nodesToTrigger.push({
764
+ node: node,
765
+ });
766
+ }
767
+ } else {
768
+ break;
769
+ }
770
+ }
771
+
772
+ return nodesToTrigger;
773
+ }
774
+
775
+ public printTopOfOrderLists(
776
+ sdkConfig: any,
777
+ clearingHouse: ClearingHouse,
778
+ slotSubscriber: SlotSubscriber,
779
+ marketIndex: BN,
780
+ marketType: MarketType
781
+ ) {
782
+ if (isVariant(marketType, 'perp')) {
783
+ const market = clearingHouse.getPerpMarketAccount(marketIndex);
784
+
785
+ const slot = slotSubscriber.getSlot();
786
+ const oraclePriceData = clearingHouse.getOracleDataForMarket(marketIndex);
787
+ const vAsk = calculateAskPrice(market, oraclePriceData);
788
+ const vBid = calculateBidPrice(market, oraclePriceData);
789
+
790
+ const bestAsk = this.getBestAsk(
791
+ marketIndex,
792
+ vAsk,
793
+ slot,
794
+ marketType,
795
+ oraclePriceData
796
+ );
797
+ const bestBid = this.getBestBid(
798
+ marketIndex,
799
+ vBid,
800
+ slot,
801
+ marketType,
802
+ oraclePriceData
803
+ );
804
+ const mid = bestAsk.add(bestBid).div(new BN(2));
805
+
806
+ const bidSpread =
807
+ (convertToNumber(bestBid, MARK_PRICE_PRECISION) /
808
+ convertToNumber(oraclePriceData.price, MARK_PRICE_PRECISION) -
809
+ 1) *
810
+ 100.0;
811
+ const askSpread =
812
+ (convertToNumber(bestAsk, MARK_PRICE_PRECISION) /
813
+ convertToNumber(oraclePriceData.price, MARK_PRICE_PRECISION) -
814
+ 1) *
815
+ 100.0;
816
+
817
+ console.log(
818
+ `Market ${sdkConfig.MARKETS[marketIndex.toNumber()].symbol} Orders`
819
+ );
820
+ console.log(
821
+ ` Ask`,
822
+ convertToNumber(bestAsk, MARK_PRICE_PRECISION).toFixed(3),
823
+ `(${askSpread.toFixed(4)}%)`
824
+ );
825
+ console.log(
826
+ ` Mid`,
827
+ convertToNumber(mid, MARK_PRICE_PRECISION).toFixed(3)
828
+ );
829
+ console.log(
830
+ ` Bid`,
831
+ convertToNumber(bestBid, MARK_PRICE_PRECISION).toFixed(3),
832
+ `(${bidSpread.toFixed(4)}%)`
833
+ );
834
+ } else if (isVariant(marketType, 'spot')) {
835
+ const slot = slotSubscriber.getSlot();
836
+ const oraclePriceData = clearingHouse.getOracleDataForMarket(marketIndex);
837
+
838
+ const bestAsk = this.getBestAsk(
839
+ marketIndex,
840
+ undefined,
841
+ slot,
842
+ marketType,
843
+ oraclePriceData
844
+ );
845
+ const bestBid = this.getBestBid(
846
+ marketIndex,
847
+ undefined,
848
+ slot,
849
+ marketType,
850
+ oraclePriceData
851
+ );
852
+ const mid = bestAsk.add(bestBid).div(new BN(2));
853
+
854
+ const bidSpread =
855
+ (convertToNumber(bestBid, MARK_PRICE_PRECISION) /
856
+ convertToNumber(oraclePriceData.price, MARK_PRICE_PRECISION) -
857
+ 1) *
858
+ 100.0;
859
+ const askSpread =
860
+ (convertToNumber(bestAsk, MARK_PRICE_PRECISION) /
861
+ convertToNumber(oraclePriceData.price, MARK_PRICE_PRECISION) -
862
+ 1) *
863
+ 100.0;
864
+
865
+ console.log(
866
+ `Market ${sdkConfig.MARKETS[marketIndex.toNumber()].symbol} Orders`
867
+ );
868
+ console.log(
869
+ ` Ask`,
870
+ convertToNumber(bestAsk, MARK_PRICE_PRECISION).toFixed(3),
871
+ `(${askSpread.toFixed(4)}%)`
872
+ );
873
+ console.log(
874
+ ` Mid`,
875
+ convertToNumber(mid, MARK_PRICE_PRECISION).toFixed(3)
876
+ );
877
+ console.log(
878
+ ` Bid`,
879
+ convertToNumber(bestBid, MARK_PRICE_PRECISION).toFixed(3),
880
+ `(${bidSpread.toFixed(4)}%)`
881
+ );
882
+ }
883
+ }
884
+ }