@velocity-exchange/sdk 0.4.0 → 0.6.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 +3 -3
- package/lib/browser/adminClient.d.ts +52 -0
- package/lib/browser/adminClient.js +97 -0
- package/lib/browser/config.js +6 -7
- package/lib/browser/constants/numericConstants.d.ts +1 -1
- package/lib/browser/constants/numericConstants.js +1 -1
- package/lib/browser/decode/user.js +5 -1
- package/lib/browser/idl/velocity.d.ts +191 -15
- package/lib/browser/idl/velocity.json +192 -16
- package/lib/browser/orderSubscriber/OrderSubscriber.d.ts +1 -0
- package/lib/browser/orderSubscriber/OrderSubscriber.js +10 -4
- package/lib/browser/pyth/pythLazerSubscriber.js +37 -27
- package/lib/browser/types.d.ts +6 -0
- package/lib/browser/user.d.ts +20 -3
- package/lib/browser/user.js +52 -4
- package/lib/browser/velocityClient.d.ts +46 -10
- package/lib/browser/velocityClient.js +107 -35
- package/lib/node/adminClient.d.ts +52 -0
- package/lib/node/adminClient.d.ts.map +1 -1
- package/lib/node/adminClient.js +97 -0
- package/lib/node/config.d.ts.map +1 -1
- package/lib/node/config.js +6 -7
- package/lib/node/constants/numericConstants.d.ts +1 -1
- package/lib/node/constants/numericConstants.d.ts.map +1 -1
- package/lib/node/constants/numericConstants.js +1 -1
- package/lib/node/decode/user.d.ts.map +1 -1
- package/lib/node/decode/user.js +5 -1
- package/lib/node/idl/velocity.d.ts +191 -15
- package/lib/node/idl/velocity.d.ts.map +1 -1
- package/lib/node/idl/velocity.json +192 -16
- package/lib/node/orderSubscriber/OrderSubscriber.d.ts +1 -0
- package/lib/node/orderSubscriber/OrderSubscriber.d.ts.map +1 -1
- package/lib/node/orderSubscriber/OrderSubscriber.js +10 -4
- package/lib/node/pyth/pythLazerSubscriber.d.ts.map +1 -1
- package/lib/node/pyth/pythLazerSubscriber.js +37 -27
- package/lib/node/types.d.ts +6 -0
- package/lib/node/types.d.ts.map +1 -1
- package/lib/node/user.d.ts +20 -3
- package/lib/node/user.d.ts.map +1 -1
- package/lib/node/user.js +52 -4
- package/lib/node/velocityClient.d.ts +46 -10
- package/lib/node/velocityClient.d.ts.map +1 -1
- package/lib/node/velocityClient.js +107 -35
- package/package.json +7 -3
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -132
- package/build-browser.js +0 -58
- package/bunfig.toml +0 -3
- package/esbuild-shims.js +0 -12
- package/scripts/deposit-isolated-positions.ts +0 -110
- package/scripts/find-flagged-users.ts +0 -213
- package/scripts/grpc-client-test-comparison.ts +0 -372
- package/scripts/grpc-multiuser-client-test-comparison.ts +0 -158
- package/scripts/postbuild.js +0 -95
- package/scripts/single-grpc-client-test.ts +0 -284
- package/scripts/withdraw-isolated-positions.ts +0 -174
- package/src/accounts/README_WebSocketAccountSubscriberV2.md +0 -95
- package/src/accounts/README_WebSocketProgramAccountSubscriberV2.md +0 -135
- package/src/accounts/basicUserAccountSubscriber.ts +0 -79
- package/src/accounts/basicUserStatsAccountSubscriber.ts +0 -87
- package/src/accounts/bulkAccountLoader.ts +0 -358
- package/src/accounts/bulkUserStatsSubscription.ts +0 -40
- package/src/accounts/bulkUserSubscription.ts +0 -39
- package/src/accounts/customizedCadenceBulkAccountLoader.ts +0 -250
- package/src/accounts/fetch.ts +0 -200
- package/src/accounts/grpcAccountSubscriber.ts +0 -225
- package/src/accounts/grpcInsuranceFundStakeAccountSubscriber.ts +0 -73
- package/src/accounts/grpcMultiAccountSubscriber.ts +0 -566
- package/src/accounts/grpcMultiUserAccountSubscriber.ts +0 -328
- package/src/accounts/grpcProgramAccountSubscriber.ts +0 -264
- package/src/accounts/grpcUserAccountSubscriber.ts +0 -63
- package/src/accounts/grpcUserStatsAccountSubscriber.ts +0 -65
- package/src/accounts/grpcVelocityClientAccountSubscriber.ts +0 -257
- package/src/accounts/grpcVelocityClientAccountSubscriberV2.ts +0 -941
- package/src/accounts/laserProgramAccountSubscriber.ts +0 -266
- package/src/accounts/oneShotUserAccountSubscriber.ts +0 -88
- package/src/accounts/oneShotUserStatsAccountSubscriber.ts +0 -94
- package/src/accounts/pollingInsuranceFundStakeAccountSubscriber.ts +0 -223
- package/src/accounts/pollingOracleAccountSubscriber.ts +0 -151
- package/src/accounts/pollingTokenAccountSubscriber.ts +0 -147
- package/src/accounts/pollingUserAccountSubscriber.ts +0 -198
- package/src/accounts/pollingUserStatsAccountSubscriber.ts +0 -180
- package/src/accounts/pollingVelocityClientAccountSubscriber.ts +0 -774
- package/src/accounts/testBulkAccountLoader.ts +0 -59
- package/src/accounts/types.ts +0 -428
- package/src/accounts/utils.ts +0 -90
- package/src/accounts/webSocketAccountSubscriber.ts +0 -304
- package/src/accounts/webSocketAccountSubscriberV2.ts +0 -536
- package/src/accounts/webSocketInsuranceFundStakeAccountSubscriber.ts +0 -171
- package/src/accounts/webSocketProgramAccountSubscriber.ts +0 -244
- package/src/accounts/webSocketProgramAccountSubscriberV2.ts +0 -1048
- package/src/accounts/webSocketProgramAccountsSubscriberV2.ts +0 -1042
- package/src/accounts/webSocketUserAccountSubscriber.ts +0 -139
- package/src/accounts/webSocketUserStatsAccountSubsriber.ts +0 -131
- package/src/accounts/webSocketVelocityClientAccountSubscriber.ts +0 -865
- package/src/accounts/webSocketVelocityClientAccountSubscriberV2.ts +0 -897
- package/src/accounts/websocketProgramUserAccountSubscriber.ts +0 -127
- package/src/addresses/marketAddresses.ts +0 -28
- package/src/addresses/pda.ts +0 -620
- package/src/adminClient.ts +0 -8136
- package/src/assert/assert.ts +0 -5
- package/src/auctionSubscriber/auctionSubscriber.ts +0 -86
- package/src/auctionSubscriber/auctionSubscriberGrpc.ts +0 -99
- package/src/auctionSubscriber/index.ts +0 -9
- package/src/auctionSubscriber/types.ts +0 -30
- package/src/bankrun/bankrunConnection.ts +0 -630
- package/src/blockhashSubscriber/BlockhashSubscriber.ts +0 -160
- package/src/blockhashSubscriber/index.ts +0 -6
- package/src/blockhashSubscriber/types.ts +0 -13
- package/src/clock/clockSubscriber.ts +0 -146
- package/src/config.ts +0 -312
- package/src/constants/index.ts +0 -9
- package/src/constants/numericConstants.ts +0 -194
- package/src/constants/perpMarkets.ts +0 -127
- package/src/constants/spotMarkets.ts +0 -104
- package/src/constants/txConstants.ts +0 -2
- package/src/constituentMap/constituentMap.ts +0 -305
- package/src/constituentMap/pollingConstituentAccountSubscriber.ts +0 -99
- package/src/constituentMap/webSocketConstituentAccountSubscriber.ts +0 -121
- package/src/core/VelocityCore.ts +0 -606
- package/src/core/index.ts +0 -12
- package/src/core/instructions/deposit.ts +0 -64
- package/src/core/instructions/fill.ts +0 -51
- package/src/core/instructions/funding.ts +0 -33
- package/src/core/instructions/liquidation.ts +0 -55
- package/src/core/instructions/orders.ts +0 -84
- package/src/core/instructions/perpOrders.ts +0 -312
- package/src/core/instructions/settlement.ts +0 -40
- package/src/core/instructions/trigger.ts +0 -41
- package/src/core/instructions/withdraw.ts +0 -65
- package/src/core/remainingAccounts.ts +0 -320
- package/src/core/signedMsg.ts +0 -109
- package/src/decode/customCoder.ts +0 -148
- package/src/decode/user.ts +0 -401
- package/src/dlob/DLOB.ts +0 -2793
- package/src/dlob/DLOBNode.ts +0 -305
- package/src/dlob/DLOBSubscriber.ts +0 -269
- package/src/dlob/NodeList.ts +0 -244
- package/src/dlob/orderBookLevels.ts +0 -695
- package/src/dlob/types.ts +0 -37
- package/src/events/eventList.ts +0 -114
- package/src/events/eventSubscriber.ts +0 -521
- package/src/events/eventsServerLogProvider.ts +0 -189
- package/src/events/fetchLogs.ts +0 -219
- package/src/events/parse.ts +0 -271
- package/src/events/pollingLogProvider.ts +0 -113
- package/src/events/sort.ts +0 -51
- package/src/events/txEventCache.ts +0 -92
- package/src/events/types.ts +0 -337
- package/src/events/webSocketLogProvider.ts +0 -152
- package/src/factory/bigNum.ts +0 -709
- package/src/factory/oracleClient.ts +0 -73
- package/src/idl/pyth.json +0 -142
- package/src/idl/pyth.ts +0 -97
- package/src/idl/pyth_solana_receiver.json +0 -628
- package/src/idl/token_faucet.json +0 -229
- package/src/idl/token_faucet.ts +0 -197
- package/src/idl/velocity.json +0 -23974
- package/src/idl/velocity.ts +0 -23980
- package/src/index.ts +0 -150
- package/src/indicative-quotes/indicativeQuotesSender.ts +0 -233
- package/src/isomorphic/README.md +0 -19
- package/src/isomorphic/anchor.browser.ts +0 -44
- package/src/isomorphic/anchor.node.ts +0 -1
- package/src/isomorphic/anchor.ts +0 -1
- package/src/isomorphic/anchor29.browser.ts +0 -24
- package/src/isomorphic/anchor29.node.ts +0 -1
- package/src/isomorphic/anchor29.ts +0 -1
- package/src/isomorphic/grpc.browser.ts +0 -4
- package/src/isomorphic/grpc.node.ts +0 -130
- package/src/isomorphic/grpc.ts +0 -1
- package/src/jupiter/jupiterClient.ts +0 -502
- package/src/keypair.ts +0 -32
- package/src/margin/README.md +0 -138
- package/src/marginCalculation.ts +0 -407
- package/src/marinade/idl/idl.json +0 -1962
- package/src/marinade/index.ts +0 -73
- package/src/marinade/types.ts +0 -3925
- package/src/math/amm.ts +0 -1758
- package/src/math/auction.ts +0 -480
- package/src/math/bankruptcy.ts +0 -138
- package/src/math/builder.ts +0 -130
- package/src/math/conversion.ts +0 -45
- package/src/math/exchangeStatus.ts +0 -261
- package/src/math/funding.ts +0 -498
- package/src/math/insurance.ts +0 -171
- package/src/math/liquidation.ts +0 -433
- package/src/math/margin.ts +0 -327
- package/src/math/market.ts +0 -487
- package/src/math/oracles.ts +0 -477
- package/src/math/orders.ts +0 -500
- package/src/math/position.ts +0 -381
- package/src/math/repeg.ts +0 -223
- package/src/math/spotBalance.ts +0 -996
- package/src/math/spotMarket.ts +0 -115
- package/src/math/spotPosition.ts +0 -263
- package/src/math/state.ts +0 -71
- package/src/math/superStake.ts +0 -602
- package/src/math/tiers.ts +0 -73
- package/src/math/trade.ts +0 -879
- package/src/math/utils.ts +0 -183
- package/src/memcmp.ts +0 -279
- package/src/oracles/oracleClientCache.ts +0 -41
- package/src/oracles/oracleId.ts +0 -99
- package/src/oracles/prelaunchOracleClient.ts +0 -63
- package/src/oracles/pythClient.ts +0 -125
- package/src/oracles/pythLazerClient.ts +0 -141
- package/src/oracles/quoteAssetOracleClient.ts +0 -43
- package/src/oracles/strictOraclePrice.ts +0 -40
- package/src/oracles/types.ts +0 -82
- package/src/oracles/utils.ts +0 -45
- package/src/orderParams.ts +0 -117
- package/src/orderSubscriber/OrderSubscriber.ts +0 -378
- package/src/orderSubscriber/PollingSubscription.ts +0 -50
- package/src/orderSubscriber/WebsocketSubscription.ts +0 -137
- package/src/orderSubscriber/grpcSubscription.ts +0 -155
- package/src/orderSubscriber/index.ts +0 -7
- package/src/orderSubscriber/types.ts +0 -64
- package/src/priorityFee/averageOverSlotsStrategy.ts +0 -17
- package/src/priorityFee/averageStrategy.ts +0 -13
- package/src/priorityFee/ewmaStrategy.ts +0 -51
- package/src/priorityFee/heliusPriorityFeeMethod.ts +0 -68
- package/src/priorityFee/index.ts +0 -16
- package/src/priorityFee/maxOverSlotsStrategy.ts +0 -18
- package/src/priorityFee/maxStrategy.ts +0 -8
- package/src/priorityFee/priorityFeeSubscriber.ts +0 -328
- package/src/priorityFee/priorityFeeSubscriberMap.ts +0 -134
- package/src/priorityFee/solanaPriorityFeeMethod.ts +0 -46
- package/src/priorityFee/types.ts +0 -82
- package/src/priorityFee/velocityPriorityFeeMethod.ts +0 -53
- package/src/pyth/constants.ts +0 -9
- package/src/pyth/index.ts +0 -15
- package/src/pyth/pythLazerSubscriber.ts +0 -365
- package/src/pyth/types.ts +0 -4451
- package/src/pyth/utils.ts +0 -13
- package/src/slot/SlotSubscriber.ts +0 -126
- package/src/slot/SlothashSubscriber.ts +0 -163
- package/src/swap/UnifiedSwapClient.ts +0 -315
- package/src/swift/grpcSignedMsgUserAccountSubscriber.ts +0 -81
- package/src/swift/index.ts +0 -10
- package/src/swift/signedMsgUserAccountSubscriber.ts +0 -251
- package/src/swift/swiftOrderSubscriber.ts +0 -351
- package/src/testClient.ts +0 -42
- package/src/titan/titanClient.ts +0 -438
- package/src/token/index.ts +0 -13
- package/src/tokenFaucet.ts +0 -296
- package/src/tx/baseTxSender.ts +0 -651
- package/src/tx/blockhashFetcher/baseBlockhashFetcher.ts +0 -31
- package/src/tx/blockhashFetcher/cachedBlockhashFetcher.ts +0 -105
- package/src/tx/blockhashFetcher/types.ts +0 -9
- package/src/tx/fastSingleTxSender.ts +0 -200
- package/src/tx/forwardOnlyTxSender.ts +0 -193
- package/src/tx/priorityFeeCalculator.ts +0 -117
- package/src/tx/reportTransactionError.ts +0 -176
- package/src/tx/retryTxSender.ts +0 -177
- package/src/tx/txHandler.ts +0 -1009
- package/src/tx/txParamProcessor.ts +0 -225
- package/src/tx/types.ts +0 -117
- package/src/tx/utils.ts +0 -133
- package/src/tx/whileValidTxSender.ts +0 -377
- package/src/types.ts +0 -2575
- package/src/user.ts +0 -5316
- package/src/userConfig.ts +0 -38
- package/src/userMap/PollingSubscription.ts +0 -61
- package/src/userMap/WebsocketSubscription.ts +0 -93
- package/src/userMap/grpcSubscription.ts +0 -93
- package/src/userMap/referrerMap.ts +0 -360
- package/src/userMap/revenueShareEscrowMap.ts +0 -342
- package/src/userMap/userMap.ts +0 -784
- package/src/userMap/userMapConfig.ts +0 -82
- package/src/userMap/userStatsMap.ts +0 -393
- package/src/userName.ts +0 -37
- package/src/userStats.ts +0 -159
- package/src/userStatsConfig.ts +0 -35
- package/src/util/TransactionConfirmationManager.ts +0 -333
- package/src/util/chainClock.ts +0 -65
- package/src/util/computeUnits.ts +0 -101
- package/src/util/digest.ts +0 -33
- package/src/util/ed25519Utils.ts +0 -95
- package/src/util/promiseTimeout.ts +0 -23
- package/src/velocityClient.ts +0 -14802
- package/src/velocityClientConfig.ts +0 -119
- package/src/wallet.ts +0 -91
- package/tests/VelocityCore/builder_escrow.test.ts +0 -121
- package/tests/VelocityCore/decode.test.ts +0 -16
- package/tests/VelocityCore/fill_trigger.test.ts +0 -73
- package/tests/VelocityCore/instructions.test.ts +0 -46
- package/tests/VelocityCore/pdas.test.ts +0 -30
- package/tests/VelocityCore/perp_orders.test.ts +0 -205
- package/tests/VelocityCore/remainingAccounts.test.ts +0 -72
- package/tests/VelocityCore/settlement_liquidation.test.ts +0 -69
- package/tests/accounts/customizedCadenceBulkAccountLoader.test.ts +0 -211
- package/tests/amm/test.ts +0 -2402
- package/tests/auctions/test.ts +0 -81
- package/tests/bn/test.ts +0 -355
- package/tests/builder/builderFee.test.ts +0 -42
- package/tests/ci/verifyConstants.ts +0 -393
- package/tests/decode/test.ts +0 -262
- package/tests/decode/userAccountBufferStrings.ts +0 -102
- package/tests/dlob/helpers.ts +0 -748
- package/tests/dlob/test.ts +0 -6969
- package/tests/dlob/tickSizeStandardization.ts +0 -545
- package/tests/events/parseLogsForCuUsage.ts +0 -139
- package/tests/exchangeStatus/test.ts +0 -45
- package/tests/insurance/test.ts +0 -40
- package/tests/liquidation/test.ts +0 -125
- package/tests/oracles/mmOracleGate.test.ts +0 -379
- package/tests/oracles/pythPegSnap.test.ts +0 -76
- package/tests/sdkParity/enumParity.test.ts +0 -84
- package/tests/sdkParity/marginCategoryFill.test.ts +0 -143
- package/tests/sdkParity/memcmpOffsets.test.ts +0 -139
- package/tests/spot/test.ts +0 -281
- package/tests/tx/TransactionConfirmationManager.test.ts +0 -312
- package/tests/tx/cachedBlockhashFetcher.test.ts +0 -96
- package/tests/tx/priorityFeeCalculator.ts +0 -77
- package/tests/tx/priorityFeeStrategy.ts +0 -95
- package/tests/user/bankruptcy.ts +0 -165
- package/tests/user/feeAndWithdrawLimits.ts +0 -284
- package/tests/user/getMarginCalculation.ts +0 -586
- package/tests/user/helpers.ts +0 -184
- package/tests/user/liquidations.ts +0 -129
- package/tests/user/marginCalculations.test.ts +0 -321
- package/tests/user/test.ts +0 -833
- package/tsconfig.browser.json +0 -18
- package/tsconfig.json +0 -36
package/src/dlob/DLOB.ts
DELETED
|
@@ -1,2793 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DLOB — Decentralized Limit Order Book.
|
|
3
|
-
*
|
|
4
|
-
* Maintains an in-memory order book built from on-chain `User` accounts.
|
|
5
|
-
* Used by keeper bots to identify the best maker orders to match against taker fills.
|
|
6
|
-
*
|
|
7
|
-
* Key types:
|
|
8
|
-
* `DLOBNode` — a single order node with price/size/user info (DLOBNode.ts)
|
|
9
|
-
* `DLOBSubscriber` — subscribes to on-chain accounts and keeps the DLOB live (DLOBSubscriber.ts)
|
|
10
|
-
* `NodeList` — sorted linked list of DLOBNodes per side/market (NodeList.ts)
|
|
11
|
-
* `orderBookLevels.ts` — aggregated L2/L3 book level construction for quoting
|
|
12
|
-
*/
|
|
13
|
-
import { getOrderSignature, NodeList } from './NodeList';
|
|
14
|
-
import { BN } from '../isomorphic/anchor';
|
|
15
|
-
import {
|
|
16
|
-
BASE_PRECISION,
|
|
17
|
-
BN_MAX,
|
|
18
|
-
PRICE_PRECISION,
|
|
19
|
-
QUOTE_PRECISION,
|
|
20
|
-
ZERO,
|
|
21
|
-
} from '../constants/numericConstants';
|
|
22
|
-
import { decodeName } from '../userName';
|
|
23
|
-
import { DLOBNode, DLOBNodeType, TriggerOrderNode } from './DLOBNode';
|
|
24
|
-
import { VelocityClient } from '../velocityClient';
|
|
25
|
-
import {
|
|
26
|
-
calculateOrderBaseAssetAmount,
|
|
27
|
-
getLimitPrice,
|
|
28
|
-
isOrderExpired,
|
|
29
|
-
isRestingLimitOrder,
|
|
30
|
-
isTriggered,
|
|
31
|
-
mustBeTriggered,
|
|
32
|
-
} from '../math/orders';
|
|
33
|
-
import {
|
|
34
|
-
getVariant,
|
|
35
|
-
isOneOfVariant,
|
|
36
|
-
isVariant,
|
|
37
|
-
MarketType,
|
|
38
|
-
MarketTypeStr,
|
|
39
|
-
Order,
|
|
40
|
-
PerpMarketAccount,
|
|
41
|
-
PositionDirection,
|
|
42
|
-
SpotMarketAccount,
|
|
43
|
-
StateAccount,
|
|
44
|
-
} from '../types';
|
|
45
|
-
import { MMOraclePriceData, OraclePriceData } from '../oracles/types';
|
|
46
|
-
import { SlotSubscriber } from '../slot/SlotSubscriber';
|
|
47
|
-
import { UserMap } from '../userMap/userMap';
|
|
48
|
-
import { PublicKey } from '@solana/web3.js';
|
|
49
|
-
import { ammPaused, exchangePaused, fillPaused } from '../math/exchangeStatus';
|
|
50
|
-
import {
|
|
51
|
-
createL2Levels,
|
|
52
|
-
getL2GeneratorFromDLOBNodes,
|
|
53
|
-
L2OrderBook,
|
|
54
|
-
L2OrderBookGenerator,
|
|
55
|
-
L3Level,
|
|
56
|
-
L3OrderBook,
|
|
57
|
-
mergeL2LevelGenerators,
|
|
58
|
-
} from './orderBookLevels';
|
|
59
|
-
import { isFallbackAvailableLiquiditySource } from '../math/auction';
|
|
60
|
-
import { convertToNumber } from '../math/conversion';
|
|
61
|
-
|
|
62
|
-
/** An on-chain order paired with the pubkey of its owning `User` account. */
|
|
63
|
-
export type DLOBOrder = { user: PublicKey; order: Order };
|
|
64
|
-
/** A list of `DLOBOrder`s, e.g. the flattened output of `DLOB.getDLOBOrders`. */
|
|
65
|
-
export type DLOBOrders = DLOBOrder[];
|
|
66
|
-
|
|
67
|
-
/** The full set of sorted `NodeList`s the DLOB maintains for one market, one per order category and side. */
|
|
68
|
-
export type MarketNodeLists = {
|
|
69
|
-
restingLimit: {
|
|
70
|
-
ask: NodeList<'restingLimit'>;
|
|
71
|
-
bid: NodeList<'restingLimit'>;
|
|
72
|
-
};
|
|
73
|
-
floatingLimit: {
|
|
74
|
-
ask: NodeList<'floatingLimit'>;
|
|
75
|
-
bid: NodeList<'floatingLimit'>;
|
|
76
|
-
};
|
|
77
|
-
takingLimit: {
|
|
78
|
-
ask: NodeList<'takingLimit'>;
|
|
79
|
-
bid: NodeList<'takingLimit'>;
|
|
80
|
-
};
|
|
81
|
-
market: {
|
|
82
|
-
ask: NodeList<'market'>;
|
|
83
|
-
bid: NodeList<'market'>;
|
|
84
|
-
};
|
|
85
|
-
trigger: {
|
|
86
|
-
above: NodeList<'trigger'>;
|
|
87
|
-
below: NodeList<'trigger'>;
|
|
88
|
-
};
|
|
89
|
-
signedMsg: {
|
|
90
|
-
ask: NodeList<'signedMsg'>;
|
|
91
|
-
bid: NodeList<'signedMsg'>;
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
type OrderBookCallback = () => void;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Receives a DLOBNode and is expected to return true if the node should
|
|
99
|
-
* be taken into account when generating, or false otherwise.
|
|
100
|
-
*
|
|
101
|
-
* Currently used in functions that rely on getBestNode
|
|
102
|
-
*/
|
|
103
|
-
export type DLOBFilterFcn = (node: DLOBNode) => boolean;
|
|
104
|
-
|
|
105
|
-
/** A taker node paired with the maker node(s) it should be filled against. `makerNodes` is empty when the fill is expected to route through fallback (e.g. vAMM) liquidity or is an expiration/cancellation rather than a maker match. */
|
|
106
|
-
export type NodeToFill = {
|
|
107
|
-
node: DLOBNode;
|
|
108
|
-
makerNodes: DLOBNode[];
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/** A trigger order whose `triggerPrice` has been crossed and is ready to be triggered on-chain. */
|
|
112
|
-
export type NodeToTrigger = {
|
|
113
|
-
node: TriggerOrderNode;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const SUPPORTED_ORDER_TYPES = [
|
|
117
|
-
'market',
|
|
118
|
-
'limit',
|
|
119
|
-
'triggerMarket',
|
|
120
|
-
'triggerLimit',
|
|
121
|
-
'oracle',
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
function getOrderOrThrow(node: DLOBNode): Order {
|
|
125
|
-
if (!node.order) {
|
|
126
|
-
throw new Error('DLOBNode must have an order');
|
|
127
|
-
}
|
|
128
|
-
return node.order;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function getUserAccountOrThrow(node: DLOBNode): string {
|
|
132
|
-
if (node.userAccount === undefined) {
|
|
133
|
-
throw new Error('DLOBNode must have a userAccount');
|
|
134
|
-
}
|
|
135
|
-
return node.userAccount;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* In-memory order book. Indexes every open order it is given into per-market, per-side sorted
|
|
140
|
-
* `NodeList`s (see `MarketNodeLists`), and provides the crossing/fill-finding logic
|
|
141
|
-
* (`findNodesToFill`) and aggregated book views (`getL2`/`getL3`) that keepers and clients use to
|
|
142
|
-
* predict and drive on-chain fills. A `DLOB` instance is normally built once per slot (e.g. via
|
|
143
|
-
* `initFromUserMap`) rather than mutated indefinitely, since state changes (`insertOrder`,
|
|
144
|
-
* `delete`) must be paired with the caller's own bookkeeping of what's already been applied.
|
|
145
|
-
*/
|
|
146
|
-
export class DLOB {
|
|
147
|
-
/** Order signatures (`getOrderSignature`) currently open, keyed by market type (`'perp'`/`'spot'`). */
|
|
148
|
-
openOrders = new Map<MarketTypeStr, Set<string>>();
|
|
149
|
-
/** Every market's `MarketNodeLists`, keyed by market type then market index. */
|
|
150
|
-
orderLists = new Map<MarketTypeStr, Map<number, MarketNodeLists>>();
|
|
151
|
-
/** The highest slot `updateRestingLimitOrders` has processed; used to skip redundant re-promotion of taking→resting orders when called with a slot that's already been seen. */
|
|
152
|
-
maxSlotForRestingLimitOrders = 0;
|
|
153
|
-
|
|
154
|
-
/** Set to `true` once `initFromUserMap` has successfully populated this instance; `initFromUserMap` is then a no-op. */
|
|
155
|
-
initialized = false;
|
|
156
|
-
|
|
157
|
-
/** Constructs an empty, uninitialized `DLOB` with no orders. Call `initFromUserMap` (or `insertOrder`/`insertSignedMsgOrder`) to populate it. */
|
|
158
|
-
public constructor() {
|
|
159
|
-
this.init();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private init() {
|
|
163
|
-
this.openOrders.set('perp', new Set<string>());
|
|
164
|
-
this.openOrders.set('spot', new Set<string>());
|
|
165
|
-
this.orderLists.set('perp', new Map<number, MarketNodeLists>());
|
|
166
|
-
this.orderLists.set('spot', new Map<number, MarketNodeLists>());
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private getOpenOrdersForMarketType(
|
|
170
|
-
marketTypeStr: MarketTypeStr
|
|
171
|
-
): Set<string> {
|
|
172
|
-
const openOrders = this.openOrders.get(marketTypeStr);
|
|
173
|
-
if (!openOrders) {
|
|
174
|
-
throw new Error(
|
|
175
|
-
`DLOB has no open orders set for market type ${marketTypeStr}`
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
return openOrders;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private getOrderListsForMarketType(
|
|
182
|
-
marketTypeStr: MarketTypeStr
|
|
183
|
-
): Map<number, MarketNodeLists> {
|
|
184
|
-
const orderLists = this.orderLists.get(marketTypeStr);
|
|
185
|
-
if (!orderLists) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
`DLOB has no order lists for market type ${marketTypeStr}`
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
return orderLists;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private tryGetMarketNodeLists(
|
|
194
|
-
marketTypeStr: MarketTypeStr,
|
|
195
|
-
marketIndex: number
|
|
196
|
-
): MarketNodeLists | undefined {
|
|
197
|
-
return this.orderLists.get(marketTypeStr)?.get(marketIndex);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private getMarketNodeLists(
|
|
201
|
-
marketTypeStr: MarketTypeStr,
|
|
202
|
-
marketIndex: number
|
|
203
|
-
): MarketNodeLists {
|
|
204
|
-
const marketNodeLists = this.tryGetMarketNodeLists(
|
|
205
|
-
marketTypeStr,
|
|
206
|
-
marketIndex
|
|
207
|
-
);
|
|
208
|
-
if (!marketNodeLists) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
`DLOB has no order lists for market type ${marketTypeStr} and market index ${marketIndex}`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
return marketNodeLists;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Empties every order list and resets the DLOB to its freshly-constructed (uninitialized) state, including `maxSlotForRestingLimitOrders` and `initialized`. */
|
|
217
|
-
public clear() {
|
|
218
|
-
for (const openOrders of this.openOrders.values()) {
|
|
219
|
-
openOrders.clear();
|
|
220
|
-
}
|
|
221
|
-
this.openOrders.clear();
|
|
222
|
-
|
|
223
|
-
for (const marketNodeListsMap of this.orderLists.values()) {
|
|
224
|
-
for (const marketNodeLists of marketNodeListsMap.values()) {
|
|
225
|
-
for (const side of Object.values(marketNodeLists)) {
|
|
226
|
-
for (const nodeList of Object.values(side)) {
|
|
227
|
-
nodeList.clear();
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
this.orderLists.clear();
|
|
233
|
-
|
|
234
|
-
this.maxSlotForRestingLimitOrders = 0;
|
|
235
|
-
|
|
236
|
-
this.init();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Populates this DLOB from every open order across every user in `userMap`. For reduce-only
|
|
241
|
-
* orders, the fillable amount is capped via `calculateOrderBaseAssetAmount` against the
|
|
242
|
-
* user's existing perp position for that market, rather than trusting the order's full
|
|
243
|
-
* stated `baseAssetAmount`. No-ops (returns `false` immediately) if this instance has already
|
|
244
|
-
* been initialized — call `clear()` first to rebuild from scratch.
|
|
245
|
-
*
|
|
246
|
-
* @param userMap map of all users' accounts to index orders from
|
|
247
|
-
* @param slot slot orders are inserted at, used to classify taking vs. resting limit orders
|
|
248
|
-
* @returns `true` if this call performed initialization, `false` if it was already initialized
|
|
249
|
-
*/
|
|
250
|
-
public async initFromUserMap(
|
|
251
|
-
userMap: UserMap,
|
|
252
|
-
slot: number
|
|
253
|
-
): Promise<boolean> {
|
|
254
|
-
if (this.initialized) {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// initialize the dlob with the user map
|
|
259
|
-
for (const user of userMap.values()) {
|
|
260
|
-
const userAccount = user.getUserAccountOrThrow();
|
|
261
|
-
const userAccountPubkey = user.getUserAccountPublicKey();
|
|
262
|
-
const userAccountPubkeyString = userAccountPubkey.toString();
|
|
263
|
-
|
|
264
|
-
for (const order of userAccount.orders) {
|
|
265
|
-
let baseAssetAmount = order.baseAssetAmount;
|
|
266
|
-
if (order.reduceOnly) {
|
|
267
|
-
const existingBaseAmount =
|
|
268
|
-
userAccount.perpPositions.find(
|
|
269
|
-
(pos) =>
|
|
270
|
-
pos.marketIndex === order.marketIndex && pos.openOrders > 0
|
|
271
|
-
)?.baseAssetAmount || ZERO;
|
|
272
|
-
baseAssetAmount = calculateOrderBaseAssetAmount(
|
|
273
|
-
order,
|
|
274
|
-
existingBaseAmount
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
this.insertOrder(order, userAccountPubkeyString, slot, baseAssetAmount);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
this.initialized = true;
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Inserts a single on-chain order into the appropriate `NodeList` for its market/side/type.
|
|
288
|
-
* No-ops if the order's status isn't `open`, or if its `orderType` isn't one of the
|
|
289
|
-
* DLOB-supported types (`market`, `limit`, `triggerMarket`, `triggerLimit`, `oracle`).
|
|
290
|
-
* Lazily creates the market's `MarketNodeLists` (via `addOrderList`) on first insert for that
|
|
291
|
-
* market. Which list the order lands in (taking vs. resting limit, floating, market, or
|
|
292
|
-
* inactive trigger) is decided by `getListForOnChainOrder`.
|
|
293
|
-
*
|
|
294
|
-
* @param order the on-chain order to insert
|
|
295
|
-
* @param userAccount base58 pubkey string of the order's owner
|
|
296
|
-
* @param slot current slot, used to classify taking vs. resting limit orders
|
|
297
|
-
* @param baseAssetAmount remaining fillable base amount, BASE_PRECISION (1e9) — for
|
|
298
|
-
* reduce-only orders this should be the position-capped amount (see
|
|
299
|
-
* `calculateOrderBaseAssetAmount`), not the raw `order.baseAssetAmount`
|
|
300
|
-
* @param onInsert optional callback invoked after a successful insert
|
|
301
|
-
*/
|
|
302
|
-
public insertOrder(
|
|
303
|
-
order: Order,
|
|
304
|
-
userAccount: string,
|
|
305
|
-
slot: number,
|
|
306
|
-
baseAssetAmount: BN,
|
|
307
|
-
onInsert?: OrderBookCallback
|
|
308
|
-
): void {
|
|
309
|
-
if (!isVariant(order.status, 'open')) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (!isOneOfVariant(order.orderType, SUPPORTED_ORDER_TYPES)) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const marketType = getVariant(order.marketType) as MarketTypeStr;
|
|
318
|
-
|
|
319
|
-
if (!this.getOrderListsForMarketType(marketType).has(order.marketIndex)) {
|
|
320
|
-
this.addOrderList(marketType, order.marketIndex);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (isVariant(order.status, 'open')) {
|
|
324
|
-
this.getOpenOrdersForMarketType(marketType).add(
|
|
325
|
-
getOrderSignature(order.orderId, userAccount)
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
this.getListForOnChainOrder(order, slot)?.insert(
|
|
330
|
-
order,
|
|
331
|
-
marketType,
|
|
332
|
-
userAccount,
|
|
333
|
-
baseAssetAmount
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
if (onInsert) {
|
|
337
|
-
onInsert();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Inserts an off-chain signed-message order (not yet landed on-chain) into the market's
|
|
343
|
-
* `signedMsg` bid/ask list, unconditionally (no status/order-type filtering, unlike
|
|
344
|
-
* `insertOrder`). Lazily creates the market's `MarketNodeLists` on first insert.
|
|
345
|
-
*
|
|
346
|
-
* @param order the signed-message order to insert
|
|
347
|
-
* @param userAccount base58 pubkey string of the order's owner
|
|
348
|
-
* @param baseAssetAmount remaining fillable base amount, BASE_PRECISION (1e9); defaults to `order.baseAssetAmount`
|
|
349
|
-
* @param onInsert optional callback invoked after insert
|
|
350
|
-
*/
|
|
351
|
-
public insertSignedMsgOrder(
|
|
352
|
-
order: Order,
|
|
353
|
-
userAccount: string,
|
|
354
|
-
baseAssetAmount?: BN,
|
|
355
|
-
onInsert?: OrderBookCallback
|
|
356
|
-
): void {
|
|
357
|
-
const marketType = getVariant(order.marketType) as MarketTypeStr;
|
|
358
|
-
const marketIndex = order.marketIndex;
|
|
359
|
-
const bidOrAsk = isVariant(order.direction, 'long') ? 'bid' : 'ask';
|
|
360
|
-
if (!this.getOrderListsForMarketType(marketType).has(order.marketIndex)) {
|
|
361
|
-
this.addOrderList(marketType, order.marketIndex);
|
|
362
|
-
}
|
|
363
|
-
this.getOpenOrdersForMarketType(marketType).add(
|
|
364
|
-
getOrderSignature(order.orderId, userAccount)
|
|
365
|
-
);
|
|
366
|
-
this.getMarketNodeLists(marketType, marketIndex).signedMsg[bidOrAsk].insert(
|
|
367
|
-
order,
|
|
368
|
-
marketType,
|
|
369
|
-
userAccount,
|
|
370
|
-
baseAssetAmount
|
|
371
|
-
);
|
|
372
|
-
if (onInsert) {
|
|
373
|
-
onInsert();
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/** Creates and registers an empty `MarketNodeLists` (all six order categories, both sides) for `marketIndex`, overwriting any existing lists for that market. */
|
|
378
|
-
addOrderList(marketType: MarketTypeStr, marketIndex: number): void {
|
|
379
|
-
this.getOrderListsForMarketType(marketType).set(marketIndex, {
|
|
380
|
-
restingLimit: {
|
|
381
|
-
ask: new NodeList('restingLimit', 'asc'),
|
|
382
|
-
bid: new NodeList('restingLimit', 'desc'),
|
|
383
|
-
},
|
|
384
|
-
floatingLimit: {
|
|
385
|
-
ask: new NodeList('floatingLimit', 'asc'),
|
|
386
|
-
bid: new NodeList('floatingLimit', 'desc'),
|
|
387
|
-
},
|
|
388
|
-
takingLimit: {
|
|
389
|
-
ask: new NodeList('takingLimit', 'asc'),
|
|
390
|
-
bid: new NodeList('takingLimit', 'asc'), // always sort ascending for market orders
|
|
391
|
-
},
|
|
392
|
-
market: {
|
|
393
|
-
ask: new NodeList('market', 'asc'),
|
|
394
|
-
bid: new NodeList('market', 'asc'), // always sort ascending for market orders
|
|
395
|
-
},
|
|
396
|
-
trigger: {
|
|
397
|
-
above: new NodeList('trigger', 'asc'),
|
|
398
|
-
below: new NodeList('trigger', 'desc'),
|
|
399
|
-
},
|
|
400
|
-
signedMsg: {
|
|
401
|
-
ask: new NodeList('signedMsg', 'asc'),
|
|
402
|
-
bid: new NodeList('signedMsg', 'asc'),
|
|
403
|
-
},
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Removes an order from whichever `NodeList` it currently lives in. No-ops if the order's
|
|
409
|
-
* status isn't `open`. First calls `updateRestingLimitOrders(slot)` so a taking-limit order
|
|
410
|
-
* that has since become a resting-limit order is looked up (and removed from) the correct
|
|
411
|
-
* list.
|
|
412
|
-
*
|
|
413
|
-
* @param order the order to remove
|
|
414
|
-
* @param userAccount pubkey of the order's owner
|
|
415
|
-
* @param slot current slot, used to resolve which list the order is currently in
|
|
416
|
-
* @param onDelete optional callback invoked after a successful delete
|
|
417
|
-
*/
|
|
418
|
-
public delete(
|
|
419
|
-
order: Order,
|
|
420
|
-
userAccount: PublicKey,
|
|
421
|
-
slot: number,
|
|
422
|
-
onDelete?: OrderBookCallback
|
|
423
|
-
): void {
|
|
424
|
-
if (!isVariant(order.status, 'open')) {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
this.updateRestingLimitOrders(slot);
|
|
429
|
-
|
|
430
|
-
this.getListForOnChainOrder(order, slot)?.remove(
|
|
431
|
-
order,
|
|
432
|
-
userAccount.toString()
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
if (onDelete) {
|
|
436
|
-
onDelete();
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Determines which `NodeList` an order belongs in, given its current state and the slot:
|
|
442
|
-
* a trigger order (`triggerMarket`/`triggerLimit`) that hasn't fired yet goes in
|
|
443
|
-
* `trigger.above`/`trigger.below`; a market/oracle-type order goes in `market`; a limit order
|
|
444
|
-
* with a non-zero `oraclePriceOffset` goes in `floatingLimit`; otherwise a limit order goes in
|
|
445
|
-
* `restingLimit` once its auction is complete or it's post-only (per `isRestingLimitOrder`),
|
|
446
|
-
* and in `takingLimit` while still auctioning.
|
|
447
|
-
*
|
|
448
|
-
* @param order the order to classify
|
|
449
|
-
* @param slot current slot, used to evaluate `isRestingLimitOrder`
|
|
450
|
-
* @returns the matching `NodeList`, or `undefined` if the order's market has no `MarketNodeLists` registered yet (e.g. `insertOrder`/`addOrderList` hasn't been called for it)
|
|
451
|
-
*/
|
|
452
|
-
public getListForOnChainOrder(
|
|
453
|
-
order: Order,
|
|
454
|
-
slot: number
|
|
455
|
-
): NodeList<any> | undefined {
|
|
456
|
-
const isInactiveTriggerOrder =
|
|
457
|
-
mustBeTriggered(order) && !isTriggered(order);
|
|
458
|
-
|
|
459
|
-
const marketType = getVariant(order.marketType) as MarketTypeStr;
|
|
460
|
-
|
|
461
|
-
const marketNodeLists = this.tryGetMarketNodeLists(
|
|
462
|
-
marketType,
|
|
463
|
-
order.marketIndex
|
|
464
|
-
);
|
|
465
|
-
if (!marketNodeLists) {
|
|
466
|
-
return undefined;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (isInactiveTriggerOrder) {
|
|
470
|
-
const subType = isVariant(order.triggerCondition, 'above')
|
|
471
|
-
? 'above'
|
|
472
|
-
: 'below';
|
|
473
|
-
return marketNodeLists.trigger[subType];
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const subType = isVariant(order.direction, 'long') ? 'bid' : 'ask';
|
|
477
|
-
if (
|
|
478
|
-
isOneOfVariant(order.orderType, ['market', 'triggerMarket', 'oracle'])
|
|
479
|
-
) {
|
|
480
|
-
return marketNodeLists.market[subType];
|
|
481
|
-
} else if (!order.oraclePriceOffset.eq(ZERO)) {
|
|
482
|
-
return marketNodeLists.floatingLimit[subType];
|
|
483
|
-
} else {
|
|
484
|
-
const isResting = isRestingLimitOrder(order, slot);
|
|
485
|
-
return isResting
|
|
486
|
-
? marketNodeLists.restingLimit[subType]
|
|
487
|
-
: marketNodeLists.takingLimit[subType];
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
private getListForOnChainOrderOrThrow(
|
|
492
|
-
order: Order,
|
|
493
|
-
slot: number
|
|
494
|
-
): NodeList<any> {
|
|
495
|
-
const list = this.getListForOnChainOrder(order, slot);
|
|
496
|
-
if (!list) {
|
|
497
|
-
throw new Error(
|
|
498
|
-
`No order list found for order ${order.orderId} in market ${order.marketIndex}`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
return list;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Promotes any `takingLimit` orders across all perp and spot markets whose auction has since
|
|
506
|
-
* completed (per `isRestingLimitOrder`) into their market's `restingLimit` list. No-ops if
|
|
507
|
-
* `slot` is not newer than the last slot this was called with (`maxSlotForRestingLimitOrders`),
|
|
508
|
-
* so it is cheap to call defensively before any read that depends on resting-limit state
|
|
509
|
-
* being current (most getters here do so internally).
|
|
510
|
-
*
|
|
511
|
-
* @param slot current slot
|
|
512
|
-
*/
|
|
513
|
-
public updateRestingLimitOrders(slot: number): void {
|
|
514
|
-
if (slot <= this.maxSlotForRestingLimitOrders) {
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
this.maxSlotForRestingLimitOrders = slot;
|
|
519
|
-
|
|
520
|
-
this.updateRestingLimitOrdersForMarketType(slot, 'perp');
|
|
521
|
-
|
|
522
|
-
this.updateRestingLimitOrdersForMarketType(slot, 'spot');
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/** Does the `takingLimit` → `restingLimit` promotion (see `updateRestingLimitOrders`) for every market of one market type. */
|
|
526
|
-
updateRestingLimitOrdersForMarketType(
|
|
527
|
-
slot: number,
|
|
528
|
-
marketTypeStr: MarketTypeStr
|
|
529
|
-
): void {
|
|
530
|
-
for (const [_, nodeLists] of this.getOrderListsForMarketType(
|
|
531
|
-
marketTypeStr
|
|
532
|
-
)) {
|
|
533
|
-
const nodesToUpdate: Array<{ side: 'ask' | 'bid'; node: DLOBNode }> = [];
|
|
534
|
-
for (const node of nodeLists.takingLimit.ask.getGenerator()) {
|
|
535
|
-
if (!isRestingLimitOrder(getOrderOrThrow(node), slot)) {
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
nodesToUpdate.push({
|
|
540
|
-
side: 'ask',
|
|
541
|
-
node,
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
for (const node of nodeLists.takingLimit.bid.getGenerator()) {
|
|
546
|
-
if (!isRestingLimitOrder(getOrderOrThrow(node), slot)) {
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
nodesToUpdate.push({
|
|
551
|
-
side: 'bid',
|
|
552
|
-
node,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
for (const nodeToUpdate of nodesToUpdate) {
|
|
557
|
-
const { side, node } = nodeToUpdate;
|
|
558
|
-
const order = getOrderOrThrow(node);
|
|
559
|
-
const userAccount = getUserAccountOrThrow(node);
|
|
560
|
-
nodeLists.takingLimit[side].remove(order, userAccount);
|
|
561
|
-
nodeLists.restingLimit[side].insert(order, marketTypeStr, userAccount);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Looks up an order by id/owner across every `NodeList` in the DLOB (perp and spot, all
|
|
568
|
-
* categories/sides) via `getNodeLists`. O(number of lists); prefer a narrower lookup (e.g.
|
|
569
|
-
* `NodeList.get`) if you already know the order's market/type.
|
|
570
|
-
*
|
|
571
|
-
* @param orderId the order's id (unique per user account)
|
|
572
|
-
* @param userAccount pubkey of the order's owner
|
|
573
|
-
* @returns the matching `Order`, or `undefined` if not found in any list
|
|
574
|
-
*/
|
|
575
|
-
public getOrder(orderId: number, userAccount: PublicKey): Order | undefined {
|
|
576
|
-
const orderSignature = getOrderSignature(orderId, userAccount.toString());
|
|
577
|
-
for (const nodeList of this.getNodeLists()) {
|
|
578
|
-
const node = nodeList.get(orderSignature);
|
|
579
|
-
if (node) {
|
|
580
|
-
return node.order;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return undefined;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Top-level entry point for keepers: finds every node in one market that is currently
|
|
589
|
-
* fillable, combining four sources — crossing resting-limit orders
|
|
590
|
-
* (`findRestingLimitOrderNodesToFill`), taking (still-auctioning) orders that cross a maker or
|
|
591
|
-
* fallback price (`findTakingNodesToFill`), expired orders to cancel/settle
|
|
592
|
-
* (`findExpiredNodesToFill`), and unfillable reduce-only orders below the step size to cancel
|
|
593
|
-
* (`findUnfillableReduceOnlyOrdersToCancel`). Returns `[]` immediately if fills are paused for
|
|
594
|
-
* this market (`fillPaused`). The market's `orderTickSize` is read from `marketAccount` and
|
|
595
|
-
* threaded through to every price comparison below so all crossing checks agree with on-chain
|
|
596
|
-
* price standardization.
|
|
597
|
-
*
|
|
598
|
-
* @param marketIndex the market to scan
|
|
599
|
-
* @param fallbackBid best available non-DLOB bid (e.g. vAMM), PRICE_PRECISION (1e6); `undefined` disables fallback-bid crossing checks
|
|
600
|
-
* @param fallbackAsk best available non-DLOB ask (e.g. vAMM), PRICE_PRECISION (1e6); `undefined` disables fallback-ask crossing checks
|
|
601
|
-
* @param slot current slot
|
|
602
|
-
* @param ts current unix timestamp (seconds), used to find expired orders
|
|
603
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`; determines whether `oraclePriceData`/`marketAccount` are typed as spot or perp
|
|
604
|
-
* @param oraclePriceData oracle price data for the market (`MMOraclePriceData` for perp, `OraclePriceData` for spot)
|
|
605
|
-
* @param stateAccount global protocol state, used for pause checks and fee-tier maker rebates
|
|
606
|
-
* @param marketAccount the market's account, used for `orderTickSize`/`orderStepSize` and pause checks
|
|
607
|
-
* @returns deduplicated `NodeToFill`s (see `mergeNodesToFill`) merging resting-limit and taking-order matches, plus expired and cancel-eligible nodes appended
|
|
608
|
-
*/
|
|
609
|
-
public findNodesToFill<T extends MarketType>(
|
|
610
|
-
marketIndex: number,
|
|
611
|
-
fallbackBid: BN | undefined,
|
|
612
|
-
fallbackAsk: BN | undefined,
|
|
613
|
-
slot: number,
|
|
614
|
-
ts: number,
|
|
615
|
-
marketType: T,
|
|
616
|
-
oraclePriceData: T extends { spot: unknown }
|
|
617
|
-
? OraclePriceData
|
|
618
|
-
: MMOraclePriceData,
|
|
619
|
-
stateAccount: StateAccount,
|
|
620
|
-
marketAccount: T extends { spot: unknown }
|
|
621
|
-
? SpotMarketAccount
|
|
622
|
-
: PerpMarketAccount
|
|
623
|
-
): NodeToFill[] {
|
|
624
|
-
if (fillPaused(stateAccount, marketAccount)) {
|
|
625
|
-
return [];
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const isAmmPaused = ammPaused(stateAccount, marketAccount);
|
|
629
|
-
|
|
630
|
-
const tickSize = (marketAccount as PerpMarketAccount | SpotMarketAccount)
|
|
631
|
-
.orderTickSize;
|
|
632
|
-
|
|
633
|
-
const { makerRebateNumerator, makerRebateDenominator } =
|
|
634
|
-
this.getMakerRebate(marketType, stateAccount, marketAccount);
|
|
635
|
-
|
|
636
|
-
const takingOrderNodesToFill: Array<NodeToFill> =
|
|
637
|
-
this.findTakingNodesToFill(
|
|
638
|
-
marketIndex,
|
|
639
|
-
slot,
|
|
640
|
-
marketType,
|
|
641
|
-
oraclePriceData,
|
|
642
|
-
isAmmPaused,
|
|
643
|
-
stateAccount,
|
|
644
|
-
marketAccount,
|
|
645
|
-
fallbackAsk,
|
|
646
|
-
fallbackBid,
|
|
647
|
-
tickSize
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
const restingLimitOrderNodesToFill: Array<NodeToFill> =
|
|
651
|
-
this.findRestingLimitOrderNodesToFill(
|
|
652
|
-
marketIndex,
|
|
653
|
-
slot,
|
|
654
|
-
marketType,
|
|
655
|
-
oraclePriceData,
|
|
656
|
-
isAmmPaused,
|
|
657
|
-
stateAccount,
|
|
658
|
-
marketAccount,
|
|
659
|
-
makerRebateNumerator,
|
|
660
|
-
makerRebateDenominator,
|
|
661
|
-
fallbackAsk,
|
|
662
|
-
fallbackBid,
|
|
663
|
-
tickSize
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
// get expired market nodes
|
|
667
|
-
const expiredNodesToFill = this.findExpiredNodesToFill(
|
|
668
|
-
marketIndex,
|
|
669
|
-
ts,
|
|
670
|
-
marketType,
|
|
671
|
-
new BN(slot)
|
|
672
|
-
);
|
|
673
|
-
|
|
674
|
-
const stepSize = isVariant(marketType, 'perp')
|
|
675
|
-
? (marketAccount as PerpMarketAccount).orderStepSize
|
|
676
|
-
: (marketAccount as SpotMarketAccount).orderStepSize;
|
|
677
|
-
|
|
678
|
-
const cancelReduceOnlyNodesToFill =
|
|
679
|
-
this.findUnfillableReduceOnlyOrdersToCancel(
|
|
680
|
-
marketIndex,
|
|
681
|
-
marketType,
|
|
682
|
-
stepSize
|
|
683
|
-
);
|
|
684
|
-
|
|
685
|
-
return this.mergeNodesToFill(
|
|
686
|
-
restingLimitOrderNodesToFill,
|
|
687
|
-
takingOrderNodesToFill
|
|
688
|
-
)
|
|
689
|
-
.concat(expiredNodesToFill)
|
|
690
|
-
.concat(cancelReduceOnlyNodesToFill);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Reads the tier-0 maker rebate fraction (`makerRebateNumerator / makerRebateDenominator`)
|
|
695
|
-
* for a market from `stateAccount`'s perp/spot fee structure, then scales the numerator up by
|
|
696
|
-
* the market's `feeAdjustment` percentage if one is set. Used by `findRestingLimitOrderNodesToFill`
|
|
697
|
-
* to size the buffer added to fallback prices so fallback fills aren't triggered by rebate-sized
|
|
698
|
-
* noise.
|
|
699
|
-
*
|
|
700
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
701
|
-
* @param stateAccount global protocol state holding the perp/spot fee tier tables
|
|
702
|
-
* @param marketAccount the specific market, whose optional `feeAdjustment` (percent) scales the rebate
|
|
703
|
-
* @returns the rebate as a numerator/denominator pair (unitless fraction, not a fixed-point BN)
|
|
704
|
-
*/
|
|
705
|
-
getMakerRebate(
|
|
706
|
-
marketType: MarketType,
|
|
707
|
-
stateAccount: StateAccount,
|
|
708
|
-
marketAccount: PerpMarketAccount | SpotMarketAccount
|
|
709
|
-
): { makerRebateNumerator: number; makerRebateDenominator: number } {
|
|
710
|
-
let makerRebateNumerator: number;
|
|
711
|
-
let makerRebateDenominator: number;
|
|
712
|
-
if (isVariant(marketType, 'perp')) {
|
|
713
|
-
makerRebateNumerator =
|
|
714
|
-
stateAccount.perpFeeStructure.feeTiers[0].makerRebateNumerator;
|
|
715
|
-
makerRebateDenominator =
|
|
716
|
-
stateAccount.perpFeeStructure.feeTiers[0].makerRebateDenominator;
|
|
717
|
-
} else {
|
|
718
|
-
makerRebateNumerator =
|
|
719
|
-
stateAccount.spotFeeStructure.feeTiers[0].makerRebateNumerator;
|
|
720
|
-
makerRebateDenominator =
|
|
721
|
-
stateAccount.spotFeeStructure.feeTiers[0].makerRebateDenominator;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// @ts-ignore
|
|
725
|
-
const feeAdjustment = marketAccount.feeAdjustment || 0;
|
|
726
|
-
if (feeAdjustment !== 0) {
|
|
727
|
-
makerRebateNumerator += (makerRebateNumerator * feeAdjustment) / 100;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
return { makerRebateNumerator, makerRebateDenominator };
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Merges two `NodeToFill` arrays (typically resting-limit crossings and taking-order
|
|
735
|
-
* crossings for the same market/pass) by taker order signature, concatenating `makerNodes`
|
|
736
|
-
* for any taker that appears in both — e.g. an order that both crosses a resting maker and
|
|
737
|
-
* separately crosses fallback liquidity ends up as one `NodeToFill` with both maker sources.
|
|
738
|
-
*
|
|
739
|
-
* @param restingLimitOrderNodesToFill fills found via resting-limit crossing
|
|
740
|
-
* @param takingOrderNodesToFill fills found via taking-order crossing
|
|
741
|
-
* @returns one `NodeToFill` per distinct taker order, with all matched maker nodes combined
|
|
742
|
-
*/
|
|
743
|
-
mergeNodesToFill(
|
|
744
|
-
restingLimitOrderNodesToFill: NodeToFill[],
|
|
745
|
-
takingOrderNodesToFill: NodeToFill[]
|
|
746
|
-
): NodeToFill[] {
|
|
747
|
-
const mergedNodesToFill = new Map<string, NodeToFill>();
|
|
748
|
-
|
|
749
|
-
const mergeNodesToFillHelper = (nodesToFillArray: NodeToFill[]) => {
|
|
750
|
-
nodesToFillArray.forEach((nodeToFill) => {
|
|
751
|
-
const nodeSignature = getOrderSignature(
|
|
752
|
-
getOrderOrThrow(nodeToFill.node).orderId,
|
|
753
|
-
getUserAccountOrThrow(nodeToFill.node)
|
|
754
|
-
);
|
|
755
|
-
|
|
756
|
-
let mergedNodeToFill = mergedNodesToFill.get(nodeSignature);
|
|
757
|
-
if (!mergedNodeToFill) {
|
|
758
|
-
mergedNodeToFill = {
|
|
759
|
-
node: nodeToFill.node,
|
|
760
|
-
makerNodes: [],
|
|
761
|
-
};
|
|
762
|
-
mergedNodesToFill.set(nodeSignature, mergedNodeToFill);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (nodeToFill.makerNodes) {
|
|
766
|
-
mergedNodeToFill.makerNodes.push(...nodeToFill.makerNodes);
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
mergeNodesToFillHelper(restingLimitOrderNodesToFill);
|
|
772
|
-
mergeNodesToFillHelper(takingOrderNodesToFill);
|
|
773
|
-
|
|
774
|
-
return Array.from(mergedNodesToFill.values());
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Finds resting-limit-order fills for a market: resting bids/asks that cross each other
|
|
779
|
-
* (`findCrossingRestingLimitOrders`), plus resting asks that cross the fallback bid and
|
|
780
|
-
* resting bids that cross the fallback ask (each skipped entirely if the AMM is paused).
|
|
781
|
-
* The fallback price on each side is tightened by the maker rebate before comparing, so a
|
|
782
|
-
* maker order priced exactly at the rebate-adjusted fallback isn't spuriously flagged as
|
|
783
|
-
* crossing (`fallbackBidWithBuffer = fallbackBid - fallbackBid * makerRebateNumerator / makerRebateDenominator`,
|
|
784
|
-
* and symmetrically for the ask).
|
|
785
|
-
*
|
|
786
|
-
* @param marketIndex the market to scan
|
|
787
|
-
* @param slot current slot
|
|
788
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
789
|
-
* @param oraclePriceData oracle price data for the market
|
|
790
|
-
* @param isAmmPaused if true, skips fallback-crossing checks (only maker-vs-maker crossings are returned)
|
|
791
|
-
* @param stateAccount global protocol state, forwarded to fallback-availability checks
|
|
792
|
-
* @param marketAccount the market's account, forwarded to fallback-availability checks
|
|
793
|
-
* @param makerRebateNumerator numerator of the maker rebate fraction (see `getMakerRebate`)
|
|
794
|
-
* @param makerRebateDenominator denominator of the maker rebate fraction (see `getMakerRebate`)
|
|
795
|
-
* @param fallbackAsk best available non-DLOB ask, PRICE_PRECISION (1e6); `undefined` skips fallback-ask crossing
|
|
796
|
-
* @param fallbackBid best available non-DLOB bid, PRICE_PRECISION (1e6); `undefined` skips fallback-bid crossing
|
|
797
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into every price comparison; omit to fall back to no rounding
|
|
798
|
-
* @returns `NodeToFill`s for maker-crossing-maker and maker-crossing-fallback matches
|
|
799
|
-
*/
|
|
800
|
-
public findRestingLimitOrderNodesToFill<T extends MarketType>(
|
|
801
|
-
marketIndex: number,
|
|
802
|
-
slot: number,
|
|
803
|
-
marketType: T,
|
|
804
|
-
oraclePriceData: T extends { spot: unknown }
|
|
805
|
-
? OraclePriceData
|
|
806
|
-
: MMOraclePriceData,
|
|
807
|
-
isAmmPaused: boolean,
|
|
808
|
-
stateAccount: StateAccount,
|
|
809
|
-
marketAccount: T extends { spot: unknown }
|
|
810
|
-
? SpotMarketAccount
|
|
811
|
-
: PerpMarketAccount,
|
|
812
|
-
makerRebateNumerator: number,
|
|
813
|
-
makerRebateDenominator: number,
|
|
814
|
-
fallbackAsk: BN | undefined,
|
|
815
|
-
fallbackBid: BN | undefined,
|
|
816
|
-
tickSize?: BN
|
|
817
|
-
): NodeToFill[] {
|
|
818
|
-
const nodesToFill = new Array<NodeToFill>();
|
|
819
|
-
|
|
820
|
-
const crossingNodes = this.findCrossingRestingLimitOrders(
|
|
821
|
-
marketIndex,
|
|
822
|
-
slot,
|
|
823
|
-
marketType,
|
|
824
|
-
oraclePriceData,
|
|
825
|
-
tickSize
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
for (const crossingNode of crossingNodes) {
|
|
829
|
-
nodesToFill.push(crossingNode);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (fallbackBid && !isAmmPaused) {
|
|
833
|
-
const askGenerator = this.getRestingLimitAsks(
|
|
834
|
-
marketIndex,
|
|
835
|
-
slot,
|
|
836
|
-
marketType,
|
|
837
|
-
oraclePriceData,
|
|
838
|
-
undefined,
|
|
839
|
-
tickSize
|
|
840
|
-
);
|
|
841
|
-
|
|
842
|
-
const fallbackBidWithBuffer = fallbackBid.sub(
|
|
843
|
-
fallbackBid.muln(makerRebateNumerator).divn(makerRebateDenominator)
|
|
844
|
-
);
|
|
845
|
-
|
|
846
|
-
const asksCrossingFallback = this.findNodesCrossingFallbackLiquidity(
|
|
847
|
-
marketType,
|
|
848
|
-
slot,
|
|
849
|
-
oraclePriceData,
|
|
850
|
-
askGenerator,
|
|
851
|
-
(askPrice) => {
|
|
852
|
-
if (askPrice === undefined) {
|
|
853
|
-
throw new Error('Resting limit ask must have a limit price');
|
|
854
|
-
}
|
|
855
|
-
return askPrice.lte(fallbackBidWithBuffer);
|
|
856
|
-
},
|
|
857
|
-
stateAccount,
|
|
858
|
-
marketAccount,
|
|
859
|
-
tickSize
|
|
860
|
-
);
|
|
861
|
-
|
|
862
|
-
for (const askCrossingFallback of asksCrossingFallback) {
|
|
863
|
-
nodesToFill.push(askCrossingFallback);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
if (fallbackAsk && !isAmmPaused) {
|
|
868
|
-
const bidGenerator = this.getRestingLimitBids(
|
|
869
|
-
marketIndex,
|
|
870
|
-
slot,
|
|
871
|
-
marketType,
|
|
872
|
-
oraclePriceData,
|
|
873
|
-
undefined,
|
|
874
|
-
tickSize
|
|
875
|
-
);
|
|
876
|
-
|
|
877
|
-
const fallbackAskWithBuffer = fallbackAsk.add(
|
|
878
|
-
fallbackAsk.muln(makerRebateNumerator).divn(makerRebateDenominator)
|
|
879
|
-
);
|
|
880
|
-
|
|
881
|
-
const bidsCrossingFallback = this.findNodesCrossingFallbackLiquidity(
|
|
882
|
-
marketType,
|
|
883
|
-
slot,
|
|
884
|
-
oraclePriceData,
|
|
885
|
-
bidGenerator,
|
|
886
|
-
(bidPrice) => {
|
|
887
|
-
if (bidPrice === undefined) {
|
|
888
|
-
throw new Error('Resting limit bid must have a limit price');
|
|
889
|
-
}
|
|
890
|
-
return bidPrice.gte(fallbackAskWithBuffer);
|
|
891
|
-
},
|
|
892
|
-
stateAccount,
|
|
893
|
-
marketAccount,
|
|
894
|
-
tickSize
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
for (const bidCrossingFallback of bidsCrossingFallback) {
|
|
898
|
-
nodesToFill.push(bidCrossingFallback);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
return nodesToFill;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Finds fills for taking (still-auctioning) orders: taking asks crossing resting bids or the
|
|
907
|
-
* fallback bid, and taking bids crossing resting asks or the fallback ask
|
|
908
|
-
* (`findTakingNodesCrossingMakerNodes` / `findNodesCrossingFallbackLiquidity`). Fallback
|
|
909
|
-
* crossing checks are skipped entirely when `isAmmPaused`. For spot markets, a taking order is
|
|
910
|
-
* only allowed to cross the opposite fallback price if doing so wouldn't also require crossing
|
|
911
|
-
* beyond the *other* fallback price (see the inline `fallbackBid`/`fallbackAsk` guards) —
|
|
912
|
-
* this prevents a taking order from routing through DLOB makers priced worse than the AMM.
|
|
913
|
-
*
|
|
914
|
-
* @param marketIndex the market to scan
|
|
915
|
-
* @param slot current slot
|
|
916
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
917
|
-
* @param oraclePriceData oracle price data for the market
|
|
918
|
-
* @param isAmmPaused if true, skips fallback-crossing checks
|
|
919
|
-
* @param state global protocol state, forwarded to fallback-availability checks
|
|
920
|
-
* @param marketAccount the market's account, forwarded to fallback-availability checks
|
|
921
|
-
* @param fallbackAsk best available non-DLOB ask, PRICE_PRECISION (1e6); `undefined` skips ask-side fallback crossing
|
|
922
|
-
* @param fallbackBid best available non-DLOB bid, PRICE_PRECISION (1e6); `undefined` skips bid-side fallback crossing
|
|
923
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into every price comparison; omit to fall back to no rounding
|
|
924
|
-
* @returns `NodeToFill`s for taking orders that cross a resting maker or fallback liquidity
|
|
925
|
-
*/
|
|
926
|
-
public findTakingNodesToFill<T extends MarketType>(
|
|
927
|
-
marketIndex: number,
|
|
928
|
-
slot: number,
|
|
929
|
-
marketType: T,
|
|
930
|
-
oraclePriceData: T extends { spot: unknown }
|
|
931
|
-
? OraclePriceData
|
|
932
|
-
: MMOraclePriceData,
|
|
933
|
-
isAmmPaused: boolean,
|
|
934
|
-
state: StateAccount,
|
|
935
|
-
marketAccount: T extends { spot: unknown }
|
|
936
|
-
? SpotMarketAccount
|
|
937
|
-
: PerpMarketAccount,
|
|
938
|
-
fallbackAsk: BN | undefined,
|
|
939
|
-
fallbackBid?: BN | undefined,
|
|
940
|
-
tickSize?: BN
|
|
941
|
-
): NodeToFill[] {
|
|
942
|
-
const nodesToFill = new Array<NodeToFill>();
|
|
943
|
-
|
|
944
|
-
let takingOrderGenerator = this.getTakingAsks(
|
|
945
|
-
marketIndex,
|
|
946
|
-
marketType,
|
|
947
|
-
slot,
|
|
948
|
-
oraclePriceData
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
const takingAsksCrossingBids = this.findTakingNodesCrossingMakerNodes(
|
|
952
|
-
marketIndex,
|
|
953
|
-
slot,
|
|
954
|
-
marketType,
|
|
955
|
-
oraclePriceData,
|
|
956
|
-
takingOrderGenerator,
|
|
957
|
-
this.getRestingLimitBids.bind(this),
|
|
958
|
-
(takerPrice, makerPrice) => {
|
|
959
|
-
if (isVariant(marketType, 'spot')) {
|
|
960
|
-
if (takerPrice === undefined) {
|
|
961
|
-
return false;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
if (fallbackBid && makerPrice.lt(fallbackBid)) {
|
|
965
|
-
return false;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return takerPrice === undefined || takerPrice.lte(makerPrice);
|
|
969
|
-
},
|
|
970
|
-
tickSize
|
|
971
|
-
);
|
|
972
|
-
for (const takingAskCrossingBid of takingAsksCrossingBids) {
|
|
973
|
-
nodesToFill.push(takingAskCrossingBid);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
if (fallbackBid && !isAmmPaused) {
|
|
977
|
-
takingOrderGenerator = this.getTakingAsks(
|
|
978
|
-
marketIndex,
|
|
979
|
-
marketType,
|
|
980
|
-
slot,
|
|
981
|
-
oraclePriceData
|
|
982
|
-
);
|
|
983
|
-
const takingAsksCrossingFallback =
|
|
984
|
-
this.findNodesCrossingFallbackLiquidity(
|
|
985
|
-
marketType,
|
|
986
|
-
slot,
|
|
987
|
-
oraclePriceData,
|
|
988
|
-
takingOrderGenerator,
|
|
989
|
-
(takerPrice) => {
|
|
990
|
-
return takerPrice === undefined || takerPrice.lte(fallbackBid);
|
|
991
|
-
},
|
|
992
|
-
state,
|
|
993
|
-
marketAccount,
|
|
994
|
-
tickSize
|
|
995
|
-
);
|
|
996
|
-
|
|
997
|
-
for (const takingAskCrossingFallback of takingAsksCrossingFallback) {
|
|
998
|
-
nodesToFill.push(takingAskCrossingFallback);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
takingOrderGenerator = this.getTakingBids(
|
|
1003
|
-
marketIndex,
|
|
1004
|
-
marketType,
|
|
1005
|
-
slot,
|
|
1006
|
-
oraclePriceData
|
|
1007
|
-
);
|
|
1008
|
-
|
|
1009
|
-
const takingBidsToFill = this.findTakingNodesCrossingMakerNodes(
|
|
1010
|
-
marketIndex,
|
|
1011
|
-
slot,
|
|
1012
|
-
marketType,
|
|
1013
|
-
oraclePriceData,
|
|
1014
|
-
takingOrderGenerator,
|
|
1015
|
-
this.getRestingLimitAsks.bind(this),
|
|
1016
|
-
(takerPrice, makerPrice) => {
|
|
1017
|
-
if (isVariant(marketType, 'spot')) {
|
|
1018
|
-
if (takerPrice === undefined) {
|
|
1019
|
-
return false;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
if (fallbackAsk && makerPrice.gt(fallbackAsk)) {
|
|
1023
|
-
return false;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
return takerPrice === undefined || takerPrice.gte(makerPrice);
|
|
1028
|
-
},
|
|
1029
|
-
tickSize
|
|
1030
|
-
);
|
|
1031
|
-
|
|
1032
|
-
for (const takingBidToFill of takingBidsToFill) {
|
|
1033
|
-
nodesToFill.push(takingBidToFill);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
if (fallbackAsk && !isAmmPaused) {
|
|
1037
|
-
takingOrderGenerator = this.getTakingBids(
|
|
1038
|
-
marketIndex,
|
|
1039
|
-
marketType,
|
|
1040
|
-
slot,
|
|
1041
|
-
oraclePriceData
|
|
1042
|
-
);
|
|
1043
|
-
const takingBidsCrossingFallback =
|
|
1044
|
-
this.findNodesCrossingFallbackLiquidity(
|
|
1045
|
-
marketType,
|
|
1046
|
-
slot,
|
|
1047
|
-
oraclePriceData,
|
|
1048
|
-
takingOrderGenerator,
|
|
1049
|
-
(takerPrice) => {
|
|
1050
|
-
return takerPrice === undefined || takerPrice.gte(fallbackAsk);
|
|
1051
|
-
},
|
|
1052
|
-
state,
|
|
1053
|
-
marketAccount,
|
|
1054
|
-
tickSize
|
|
1055
|
-
);
|
|
1056
|
-
for (const marketBidCrossingFallback of takingBidsCrossingFallback) {
|
|
1057
|
-
nodesToFill.push(marketBidCrossingFallback);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return nodesToFill;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* Walks `takerNodeGenerator` (taking bids or asks, sorted by arrival slot) against a fresh
|
|
1066
|
-
* maker-side generator (built per taker via `makerNodeGeneratorFn`, e.g.
|
|
1067
|
-
* `getRestingLimitBids`) and records a `NodeToFill` for every taker/maker pair that
|
|
1068
|
-
* `doesCross` accepts, skipping same-user matches. For each match this method also **mutates
|
|
1069
|
-
* DLOB state**: it applies the simulated fill to both the maker's and taker's order lists
|
|
1070
|
-
* (via `NodeList.update`) so subsequent iterations see updated `baseAssetAmountFilled` and a
|
|
1071
|
-
* taker stops matching once fully filled. Because maker nodes (sorted by price) are scanned
|
|
1072
|
-
* in order, `doesCross` returning false breaks out of the maker loop entirely — this is
|
|
1073
|
-
* correct for resting-limit makers but relies on the maker generator being price-sorted, not
|
|
1074
|
-
* time-sorted.
|
|
1075
|
-
*
|
|
1076
|
-
* @param marketIndex the market being scanned (used only to route signed-message taker fills into the right list)
|
|
1077
|
-
* @param slot current slot, forwarded to price lookups
|
|
1078
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1079
|
-
* @param oraclePriceData oracle price data for the market
|
|
1080
|
-
* @param takerNodeGenerator taking orders to check, e.g. from `getTakingAsks`/`getTakingBids`
|
|
1081
|
-
* @param makerNodeGeneratorFn factory invoked once per taker to get a fresh maker-side generator, e.g. `getRestingLimitBids`/`getRestingLimitAsks`
|
|
1082
|
-
* @param doesCross given the taker's price (`undefined` if it has none, e.g. still mid-auction) and the maker's price, returns whether they cross
|
|
1083
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into all price lookups
|
|
1084
|
-
* @returns one `NodeToFill` per matched taker/maker pair (a taker matched against multiple makers yields multiple entries, each with one maker)
|
|
1085
|
-
*/
|
|
1086
|
-
public findTakingNodesCrossingMakerNodes<T extends MarketType>(
|
|
1087
|
-
marketIndex: number,
|
|
1088
|
-
slot: number,
|
|
1089
|
-
marketType: T,
|
|
1090
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1091
|
-
? OraclePriceData
|
|
1092
|
-
: MMOraclePriceData,
|
|
1093
|
-
takerNodeGenerator: Generator<DLOBNode>,
|
|
1094
|
-
makerNodeGeneratorFn: (
|
|
1095
|
-
marketIndex: number,
|
|
1096
|
-
slot: number,
|
|
1097
|
-
marketType: T,
|
|
1098
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1099
|
-
? OraclePriceData
|
|
1100
|
-
: MMOraclePriceData,
|
|
1101
|
-
filterFcn?: DLOBFilterFcn,
|
|
1102
|
-
tickSize?: BN
|
|
1103
|
-
) => Generator<DLOBNode>,
|
|
1104
|
-
doesCross: (takerPrice: BN | undefined, makerPrice: BN) => boolean,
|
|
1105
|
-
tickSize?: BN
|
|
1106
|
-
): NodeToFill[] {
|
|
1107
|
-
const nodesToFill = new Array<NodeToFill>();
|
|
1108
|
-
|
|
1109
|
-
for (const takerNode of takerNodeGenerator) {
|
|
1110
|
-
const makerNodeGenerator = makerNodeGeneratorFn(
|
|
1111
|
-
marketIndex,
|
|
1112
|
-
slot,
|
|
1113
|
-
marketType,
|
|
1114
|
-
oraclePriceData,
|
|
1115
|
-
undefined,
|
|
1116
|
-
tickSize
|
|
1117
|
-
);
|
|
1118
|
-
|
|
1119
|
-
for (const makerNode of makerNodeGenerator) {
|
|
1120
|
-
// Can't match orders from the same user
|
|
1121
|
-
const sameUser = takerNode.userAccount === makerNode.userAccount;
|
|
1122
|
-
if (sameUser) {
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const makerPrice = makerNode.getPriceOrThrow(
|
|
1127
|
-
oraclePriceData,
|
|
1128
|
-
slot,
|
|
1129
|
-
tickSize
|
|
1130
|
-
);
|
|
1131
|
-
const takerPrice = takerNode.getPrice(oraclePriceData, slot, tickSize);
|
|
1132
|
-
|
|
1133
|
-
const ordersCross = doesCross(takerPrice, makerPrice);
|
|
1134
|
-
if (!ordersCross) {
|
|
1135
|
-
// market orders aren't sorted by price, they are sorted by time, so we need to traverse
|
|
1136
|
-
// through all of em
|
|
1137
|
-
break;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
nodesToFill.push({
|
|
1141
|
-
node: takerNode,
|
|
1142
|
-
makerNodes: [makerNode],
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
const makerOrder = getOrderOrThrow(makerNode);
|
|
1146
|
-
const takerOrder = getOrderOrThrow(takerNode);
|
|
1147
|
-
|
|
1148
|
-
const makerBaseRemaining = makerOrder.baseAssetAmount.sub(
|
|
1149
|
-
makerOrder.baseAssetAmountFilled
|
|
1150
|
-
);
|
|
1151
|
-
const takerBaseRemaining = takerOrder.baseAssetAmount.sub(
|
|
1152
|
-
takerOrder.baseAssetAmountFilled
|
|
1153
|
-
);
|
|
1154
|
-
|
|
1155
|
-
const baseFilled = BN.min(makerBaseRemaining, takerBaseRemaining);
|
|
1156
|
-
|
|
1157
|
-
const newMakerOrder = { ...makerOrder };
|
|
1158
|
-
newMakerOrder.baseAssetAmountFilled =
|
|
1159
|
-
makerOrder.baseAssetAmountFilled.add(baseFilled);
|
|
1160
|
-
this.getListForOnChainOrderOrThrow(newMakerOrder, slot).update(
|
|
1161
|
-
newMakerOrder,
|
|
1162
|
-
getUserAccountOrThrow(makerNode)
|
|
1163
|
-
);
|
|
1164
|
-
|
|
1165
|
-
const newTakerOrder = { ...takerOrder };
|
|
1166
|
-
newTakerOrder.baseAssetAmountFilled =
|
|
1167
|
-
takerOrder.baseAssetAmountFilled.add(baseFilled);
|
|
1168
|
-
|
|
1169
|
-
if (takerNode.isSignedMsg) {
|
|
1170
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
1171
|
-
const marketNodeLists = this.getMarketNodeLists(
|
|
1172
|
-
marketTypeStr,
|
|
1173
|
-
marketIndex
|
|
1174
|
-
);
|
|
1175
|
-
const orderList = isVariant(takerOrder.direction, 'long')
|
|
1176
|
-
? marketNodeLists.signedMsg.bid
|
|
1177
|
-
: marketNodeLists.signedMsg.ask;
|
|
1178
|
-
orderList.update(newTakerOrder, getUserAccountOrThrow(takerNode));
|
|
1179
|
-
} else {
|
|
1180
|
-
this.getListForOnChainOrderOrThrow(newTakerOrder, slot).update(
|
|
1181
|
-
newTakerOrder,
|
|
1182
|
-
getUserAccountOrThrow(takerNode)
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
if (
|
|
1187
|
-
newTakerOrder.baseAssetAmountFilled.eq(takerOrder.baseAssetAmount)
|
|
1188
|
-
) {
|
|
1189
|
-
break;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
return nodesToFill;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
/**
|
|
1198
|
-
* Scans `nodeGenerator` for nodes that both cross the fallback price (`doesCross`, evaluated
|
|
1199
|
-
* against each node's `getLimitPrice`, or crossing unconditionally if the node has no limit
|
|
1200
|
-
* price) and have fallback liquidity actually available to fill against. For spot markets,
|
|
1201
|
-
* post-only orders are skipped (they can never take against the AMM) and fallback liquidity
|
|
1202
|
-
* is always considered available; for perp markets, availability additionally requires
|
|
1203
|
-
* `isFallbackAvailableLiquiditySource` (broadly: the order's auction is complete and the
|
|
1204
|
-
* oracle is valid enough for AMM fills). Does not mutate any order state — unlike
|
|
1205
|
-
* `findTakingNodesCrossingMakerNodes`, fallback fills are expected to be sized/settled
|
|
1206
|
-
* on-chain rather than simulated here.
|
|
1207
|
-
*
|
|
1208
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1209
|
-
* @param slot current slot
|
|
1210
|
-
* @param oraclePriceData oracle price data for the market
|
|
1211
|
-
* @param nodeGenerator candidate nodes to check, e.g. resting-limit or taking orders
|
|
1212
|
-
* @param doesCross given a node's limit price (`undefined` if it has none), returns whether it crosses the fallback price
|
|
1213
|
-
* @param state global protocol state, used by the perp fallback-availability check
|
|
1214
|
-
* @param marketAccount the market's account, used by the perp fallback-availability check
|
|
1215
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6), used to resolve each node's limit price
|
|
1216
|
-
* @returns `NodeToFill`s with an empty `makerNodes` array (the fill is expected to route through fallback liquidity, not a DLOB maker)
|
|
1217
|
-
*/
|
|
1218
|
-
public findNodesCrossingFallbackLiquidity<T extends MarketType>(
|
|
1219
|
-
marketType: T,
|
|
1220
|
-
slot: number,
|
|
1221
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1222
|
-
? OraclePriceData
|
|
1223
|
-
: MMOraclePriceData,
|
|
1224
|
-
nodeGenerator: Generator<DLOBNode>,
|
|
1225
|
-
doesCross: (nodePrice: BN | undefined) => boolean,
|
|
1226
|
-
state: StateAccount,
|
|
1227
|
-
marketAccount: T extends { spot: unknown }
|
|
1228
|
-
? SpotMarketAccount
|
|
1229
|
-
: PerpMarketAccount,
|
|
1230
|
-
tickSize?: BN
|
|
1231
|
-
): NodeToFill[] {
|
|
1232
|
-
const nodesToFill = new Array<NodeToFill>();
|
|
1233
|
-
|
|
1234
|
-
let nextNode = nodeGenerator.next();
|
|
1235
|
-
while (!nextNode.done) {
|
|
1236
|
-
const node = nextNode.value;
|
|
1237
|
-
|
|
1238
|
-
if (isVariant(marketType, 'spot') && node.order?.postOnly) {
|
|
1239
|
-
nextNode = nodeGenerator.next();
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
const nodeOrder = getOrderOrThrow(node);
|
|
1243
|
-
const nodePrice = getLimitPrice(
|
|
1244
|
-
nodeOrder,
|
|
1245
|
-
oraclePriceData,
|
|
1246
|
-
slot,
|
|
1247
|
-
undefined,
|
|
1248
|
-
tickSize
|
|
1249
|
-
);
|
|
1250
|
-
|
|
1251
|
-
// order crosses if there is no limit price or it crosses fallback price
|
|
1252
|
-
const crosses = doesCross(nodePrice);
|
|
1253
|
-
|
|
1254
|
-
// fallback is available if auction is complete or it's a spot order
|
|
1255
|
-
const fallbackAvailable =
|
|
1256
|
-
isVariant(marketType, 'spot') ||
|
|
1257
|
-
isFallbackAvailableLiquiditySource(
|
|
1258
|
-
nodeOrder,
|
|
1259
|
-
oraclePriceData as MMOraclePriceData,
|
|
1260
|
-
slot,
|
|
1261
|
-
state,
|
|
1262
|
-
marketAccount as PerpMarketAccount
|
|
1263
|
-
);
|
|
1264
|
-
|
|
1265
|
-
if (crosses && fallbackAvailable) {
|
|
1266
|
-
nodesToFill.push({
|
|
1267
|
-
node: node,
|
|
1268
|
-
makerNodes: [], // filled by fallback
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
nextNode = nodeGenerator.next();
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
return nodesToFill;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
/**
|
|
1279
|
-
* Finds orders in a market that are eligible to be expired: any non-trigger, non-TIF-limit
|
|
1280
|
-
* order whose `maxTs` (plus a 25-second buffer for limit orders, via `isOrderExpired`) has
|
|
1281
|
-
* passed the given timestamp. Also proactively removes (not just reports) signed-message
|
|
1282
|
-
* orders whose auction window (`order.slot + order.auctionDuration`) has passed `slot`, since
|
|
1283
|
-
* those never landed on-chain and have no on-chain expiration to wait for.
|
|
1284
|
-
*
|
|
1285
|
-
* @param marketIndex the market to scan
|
|
1286
|
-
* @param ts current unix timestamp (seconds)
|
|
1287
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1288
|
-
* @param slot current slot; required if the market has any signed-message orders (throws otherwise)
|
|
1289
|
-
* @returns `NodeToFill`s (with empty `makerNodes`) for orders ready to expire
|
|
1290
|
-
* @throws if a signed-message order is present and `slot` was not provided
|
|
1291
|
-
*/
|
|
1292
|
-
public findExpiredNodesToFill(
|
|
1293
|
-
marketIndex: number,
|
|
1294
|
-
ts: number,
|
|
1295
|
-
marketType: MarketType,
|
|
1296
|
-
slot?: BN
|
|
1297
|
-
): NodeToFill[] {
|
|
1298
|
-
const nodesToFill = new Array<NodeToFill>();
|
|
1299
|
-
|
|
1300
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
1301
|
-
const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
|
|
1302
|
-
|
|
1303
|
-
if (!nodeLists) {
|
|
1304
|
-
return nodesToFill;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// All bids/asks that can expire
|
|
1308
|
-
// dont try to expire limit orders with tif as its inefficient use of blockspace
|
|
1309
|
-
const bidGenerators = [
|
|
1310
|
-
nodeLists.takingLimit.bid.getGenerator(),
|
|
1311
|
-
nodeLists.restingLimit.bid.getGenerator(),
|
|
1312
|
-
nodeLists.floatingLimit.bid.getGenerator(),
|
|
1313
|
-
nodeLists.market.bid.getGenerator(),
|
|
1314
|
-
nodeLists.signedMsg.bid.getGenerator(),
|
|
1315
|
-
];
|
|
1316
|
-
const askGenerators = [
|
|
1317
|
-
nodeLists.takingLimit.ask.getGenerator(),
|
|
1318
|
-
nodeLists.restingLimit.ask.getGenerator(),
|
|
1319
|
-
nodeLists.floatingLimit.ask.getGenerator(),
|
|
1320
|
-
nodeLists.market.ask.getGenerator(),
|
|
1321
|
-
nodeLists.signedMsg.ask.getGenerator(),
|
|
1322
|
-
];
|
|
1323
|
-
|
|
1324
|
-
for (const bidGenerator of bidGenerators) {
|
|
1325
|
-
for (const bid of bidGenerator) {
|
|
1326
|
-
const bidOrder = getOrderOrThrow(bid);
|
|
1327
|
-
if (bid.isSignedMsg) {
|
|
1328
|
-
if (slot === undefined) {
|
|
1329
|
-
throw new Error(
|
|
1330
|
-
'Must provide slot to findExpiredNodesToFill to expire signedMsg orders'
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
if (slot.gt(bidOrder.slot.addn(bidOrder.auctionDuration))) {
|
|
1334
|
-
nodeLists.signedMsg.bid.remove(
|
|
1335
|
-
bidOrder,
|
|
1336
|
-
getUserAccountOrThrow(bid)
|
|
1337
|
-
);
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
if (isOrderExpired(bidOrder, ts, true, 25)) {
|
|
1342
|
-
nodesToFill.push({
|
|
1343
|
-
node: bid,
|
|
1344
|
-
makerNodes: [],
|
|
1345
|
-
});
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
for (const askGenerator of askGenerators) {
|
|
1351
|
-
for (const ask of askGenerator) {
|
|
1352
|
-
const askOrder = getOrderOrThrow(ask);
|
|
1353
|
-
if (ask.isSignedMsg) {
|
|
1354
|
-
if (slot === undefined) {
|
|
1355
|
-
throw new Error(
|
|
1356
|
-
'Must provide slot to findExpiredNodesToFill to expire signedMsg orders'
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
if (slot.gt(askOrder.slot.addn(askOrder.auctionDuration))) {
|
|
1360
|
-
nodeLists.signedMsg.ask.remove(
|
|
1361
|
-
askOrder,
|
|
1362
|
-
getUserAccountOrThrow(ask)
|
|
1363
|
-
);
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
if (isOrderExpired(askOrder, ts, true, 25)) {
|
|
1368
|
-
nodesToFill.push({
|
|
1369
|
-
node: ask,
|
|
1370
|
-
makerNodes: [],
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
return nodesToFill;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
/**
|
|
1380
|
-
* Finds reduce-only orders across every category/side in a market whose remaining
|
|
1381
|
-
* `baseAssetAmount` (as tracked on the node, not necessarily the order's original size) has
|
|
1382
|
-
* dropped below the market's minimum step size — meaning the order can never be filled again
|
|
1383
|
-
* and should be canceled by a keeper rather than left to linger.
|
|
1384
|
-
*
|
|
1385
|
-
* @param marketIndex the market to scan
|
|
1386
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1387
|
-
* @param stepSize market's minimum order step size, BASE_PRECISION (1e9)
|
|
1388
|
-
* @returns `NodeToFill`s (with empty `makerNodes`) for reduce-only orders that should be canceled
|
|
1389
|
-
*/
|
|
1390
|
-
public findUnfillableReduceOnlyOrdersToCancel(
|
|
1391
|
-
marketIndex: number,
|
|
1392
|
-
marketType: MarketType,
|
|
1393
|
-
stepSize: BN
|
|
1394
|
-
): NodeToFill[] {
|
|
1395
|
-
const nodesToFill = new Array<NodeToFill>();
|
|
1396
|
-
|
|
1397
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
1398
|
-
const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
|
|
1399
|
-
|
|
1400
|
-
if (!nodeLists) {
|
|
1401
|
-
return nodesToFill;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
const generators = [
|
|
1405
|
-
nodeLists.takingLimit.bid.getGenerator(),
|
|
1406
|
-
nodeLists.restingLimit.bid.getGenerator(),
|
|
1407
|
-
nodeLists.floatingLimit.bid.getGenerator(),
|
|
1408
|
-
nodeLists.market.bid.getGenerator(),
|
|
1409
|
-
nodeLists.signedMsg.bid.getGenerator(),
|
|
1410
|
-
nodeLists.takingLimit.ask.getGenerator(),
|
|
1411
|
-
nodeLists.restingLimit.ask.getGenerator(),
|
|
1412
|
-
nodeLists.floatingLimit.ask.getGenerator(),
|
|
1413
|
-
nodeLists.market.ask.getGenerator(),
|
|
1414
|
-
nodeLists.signedMsg.ask.getGenerator(),
|
|
1415
|
-
nodeLists.trigger.above.getGenerator(),
|
|
1416
|
-
nodeLists.trigger.below.getGenerator(),
|
|
1417
|
-
];
|
|
1418
|
-
|
|
1419
|
-
for (const generator of generators) {
|
|
1420
|
-
for (const node of generator) {
|
|
1421
|
-
if (!getOrderOrThrow(node).reduceOnly) {
|
|
1422
|
-
continue;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
if (node.baseAssetAmount.lt(stepSize)) {
|
|
1426
|
-
nodesToFill.push({
|
|
1427
|
-
node,
|
|
1428
|
-
makerNodes: [],
|
|
1429
|
-
});
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
return nodesToFill;
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Yields taking (still-auctioning) bid nodes for a market — market-bid orders, taking-limit
|
|
1438
|
-
* bids, and signed-message bids not yet resting — merged in arrival order (earliest `slot`
|
|
1439
|
-
* first, via `getBestNode`). Calls `updateRestingLimitOrders(slot)` first so a signed-message
|
|
1440
|
-
* order that has since become a resting-limit order is excluded here.
|
|
1441
|
-
*
|
|
1442
|
-
* @param marketIndex the market to scan
|
|
1443
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1444
|
-
* @param slot current slot
|
|
1445
|
-
* @param oraclePriceData oracle price data for the market (unused for the ordering itself, forwarded to `getBestNode`)
|
|
1446
|
-
* @param filterFcn optional predicate; nodes it rejects are skipped and not yielded
|
|
1447
|
-
* @returns a generator of taking bid nodes, or an empty generator if the market has no `MarketNodeLists`
|
|
1448
|
-
*/
|
|
1449
|
-
*getTakingBids<T extends MarketType>(
|
|
1450
|
-
marketIndex: number,
|
|
1451
|
-
marketType: T,
|
|
1452
|
-
slot: number,
|
|
1453
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1454
|
-
? OraclePriceData
|
|
1455
|
-
: MMOraclePriceData,
|
|
1456
|
-
filterFcn?: DLOBFilterFcn
|
|
1457
|
-
): Generator<DLOBNode> {
|
|
1458
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
1459
|
-
const orderLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
|
|
1460
|
-
if (!orderLists) {
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
this.updateRestingLimitOrders(slot);
|
|
1465
|
-
|
|
1466
|
-
const generatorList = [
|
|
1467
|
-
orderLists.market.bid.getGenerator(),
|
|
1468
|
-
orderLists.takingLimit.bid.getGenerator(),
|
|
1469
|
-
this.signedMsgGenerator(
|
|
1470
|
-
orderLists.signedMsg.bid,
|
|
1471
|
-
(x: DLOBNode) => !isRestingLimitOrder(getOrderOrThrow(x), slot)
|
|
1472
|
-
),
|
|
1473
|
-
];
|
|
1474
|
-
|
|
1475
|
-
yield* this.getBestNode(
|
|
1476
|
-
generatorList,
|
|
1477
|
-
oraclePriceData,
|
|
1478
|
-
slot,
|
|
1479
|
-
(bestNode, currentNode) => {
|
|
1480
|
-
return getOrderOrThrow(bestNode).slot.lt(
|
|
1481
|
-
getOrderOrThrow(currentNode).slot
|
|
1482
|
-
);
|
|
1483
|
-
},
|
|
1484
|
-
filterFcn
|
|
1485
|
-
);
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
/** Same as `getTakingBids`, but for the ask side. */
|
|
1489
|
-
*getTakingAsks<T extends MarketType>(
|
|
1490
|
-
marketIndex: number,
|
|
1491
|
-
marketType: T,
|
|
1492
|
-
slot: number,
|
|
1493
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1494
|
-
? OraclePriceData
|
|
1495
|
-
: MMOraclePriceData,
|
|
1496
|
-
filterFcn?: DLOBFilterFcn
|
|
1497
|
-
): Generator<DLOBNode> {
|
|
1498
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
1499
|
-
const orderLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
|
|
1500
|
-
if (!orderLists) {
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
this.updateRestingLimitOrders(slot);
|
|
1505
|
-
|
|
1506
|
-
const generatorList = [
|
|
1507
|
-
orderLists.market.ask.getGenerator(),
|
|
1508
|
-
orderLists.takingLimit.ask.getGenerator(),
|
|
1509
|
-
this.signedMsgGenerator(
|
|
1510
|
-
orderLists.signedMsg.ask,
|
|
1511
|
-
(x: DLOBNode) => !isRestingLimitOrder(getOrderOrThrow(x), slot)
|
|
1512
|
-
),
|
|
1513
|
-
];
|
|
1514
|
-
|
|
1515
|
-
yield* this.getBestNode(
|
|
1516
|
-
generatorList,
|
|
1517
|
-
oraclePriceData,
|
|
1518
|
-
slot,
|
|
1519
|
-
(bestNode, currentNode) => {
|
|
1520
|
-
return getOrderOrThrow(bestNode).slot.lt(
|
|
1521
|
-
getOrderOrThrow(currentNode).slot
|
|
1522
|
-
);
|
|
1523
|
-
},
|
|
1524
|
-
filterFcn
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
/** Filters a `signedMsg` `NodeList`'s nodes by an arbitrary predicate — used to split signed-message orders into "still taking" vs. "now resting" subsets based on `isRestingLimitOrder`. */
|
|
1529
|
-
protected *signedMsgGenerator(
|
|
1530
|
-
signedMsgOrderList: NodeList<'signedMsg'>,
|
|
1531
|
-
filter: (x: DLOBNode) => boolean
|
|
1532
|
-
): Generator<DLOBNode> {
|
|
1533
|
-
for (const signedMsgOrder of signedMsgOrderList.getGenerator()) {
|
|
1534
|
-
if (filter(signedMsgOrder)) {
|
|
1535
|
-
yield signedMsgOrder;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
/**
|
|
1541
|
-
* K-way-merges multiple node generators (e.g. one per order category feeding one side of the
|
|
1542
|
-
* book) into a single generator ordered by `compareFcn`, skipping nodes that are already
|
|
1543
|
-
* fully filled (`isBaseFilled`) or rejected by `filterFcn`. This is the shared core behind
|
|
1544
|
-
* `getTakingBids`/`getTakingAsks`/`getRestingLimitBids`/`getRestingLimitAsks`/`getBids`/`getAsks`
|
|
1545
|
-
* — each just supplies a different `generatorList` and `compareFcn`.
|
|
1546
|
-
*
|
|
1547
|
-
* @param generatorList the node generators to merge; each must already be sorted per `compareFcn`
|
|
1548
|
-
* @param oraclePriceData oracle price data, forwarded to `compareFcn`
|
|
1549
|
-
* @param slot current slot, forwarded to `compareFcn`
|
|
1550
|
-
* @param compareFcn returns true if `bestDLOBNode` should be preferred over `currentDLOBNode`
|
|
1551
|
-
* @param filterFcn optional predicate; nodes it rejects are advanced past and not yielded
|
|
1552
|
-
* @returns a single generator yielding the merged, filtered, non-fully-filled nodes in `compareFcn` order
|
|
1553
|
-
*/
|
|
1554
|
-
protected *getBestNode<T extends MarketTypeStr>(
|
|
1555
|
-
generatorList: Array<Generator<DLOBNode>>,
|
|
1556
|
-
oraclePriceData: T extends 'spot' ? OraclePriceData : MMOraclePriceData,
|
|
1557
|
-
slot: number,
|
|
1558
|
-
compareFcn: (
|
|
1559
|
-
bestDLOBNode: DLOBNode,
|
|
1560
|
-
currentDLOBNode: DLOBNode,
|
|
1561
|
-
slot: number,
|
|
1562
|
-
oraclePriceData: T extends 'spot' ? OraclePriceData : MMOraclePriceData
|
|
1563
|
-
) => boolean,
|
|
1564
|
-
filterFcn?: DLOBFilterFcn
|
|
1565
|
-
): Generator<DLOBNode> {
|
|
1566
|
-
const generators = generatorList.map((generator) => {
|
|
1567
|
-
return {
|
|
1568
|
-
next: generator.next(),
|
|
1569
|
-
generator,
|
|
1570
|
-
};
|
|
1571
|
-
});
|
|
1572
|
-
|
|
1573
|
-
let sideExhausted = false;
|
|
1574
|
-
while (!sideExhausted) {
|
|
1575
|
-
const bestGenerator = generators.reduce(
|
|
1576
|
-
(bestGenerator, currentGenerator) => {
|
|
1577
|
-
if (currentGenerator.next.done) {
|
|
1578
|
-
return bestGenerator;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
if (bestGenerator.next.done) {
|
|
1582
|
-
return currentGenerator;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
const bestValue = bestGenerator.next.value as DLOBNode;
|
|
1586
|
-
const currentValue = currentGenerator.next.value as DLOBNode;
|
|
1587
|
-
|
|
1588
|
-
return compareFcn(bestValue, currentValue, slot, oraclePriceData)
|
|
1589
|
-
? bestGenerator
|
|
1590
|
-
: currentGenerator;
|
|
1591
|
-
}
|
|
1592
|
-
);
|
|
1593
|
-
|
|
1594
|
-
if (!bestGenerator.next.done) {
|
|
1595
|
-
// skip this node if it's already completely filled
|
|
1596
|
-
if (bestGenerator.next.value.isBaseFilled()) {
|
|
1597
|
-
bestGenerator.next = bestGenerator.generator.next();
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
if (filterFcn && !filterFcn(bestGenerator.next.value)) {
|
|
1602
|
-
bestGenerator.next = bestGenerator.generator.next();
|
|
1603
|
-
continue;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
yield bestGenerator.next.value;
|
|
1607
|
-
bestGenerator.next = bestGenerator.generator.next();
|
|
1608
|
-
} else {
|
|
1609
|
-
sideExhausted = true;
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
/**
|
|
1615
|
-
* Yields resting-limit ask nodes for a market — `restingLimit`, `floatingLimit`, and any
|
|
1616
|
-
* `signedMsg` asks that have become resting — merged best-price-first (lowest ask price
|
|
1617
|
-
* first, ties broken by `getBestNode`'s underlying comparator). Calls
|
|
1618
|
-
* `updateRestingLimitOrders(slot)` first. `tickSize` is threaded into every price comparison
|
|
1619
|
-
* via `DLOBNode.getPriceOrThrow`, so pass the market's `orderTickSize` to match on-chain
|
|
1620
|
-
* price standardization — omitting it defaults to no rounding (tick of 1).
|
|
1621
|
-
*
|
|
1622
|
-
* @param marketIndex the market to scan
|
|
1623
|
-
* @param slot current slot
|
|
1624
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1625
|
-
* @param oraclePriceData oracle price data for the market; required for spot markets (throws if missing)
|
|
1626
|
-
* @param filterFcn optional predicate; nodes it rejects are skipped
|
|
1627
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
1628
|
-
* @returns a generator of resting-limit ask nodes, best price first
|
|
1629
|
-
* @throws if `marketType` is spot and `oraclePriceData` is not provided; also throws (via `getPriceOrThrow`) if any node has no resolvable limit price
|
|
1630
|
-
*/
|
|
1631
|
-
*getRestingLimitAsks<T extends MarketType>(
|
|
1632
|
-
marketIndex: number,
|
|
1633
|
-
slot: number,
|
|
1634
|
-
marketType: T,
|
|
1635
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1636
|
-
? OraclePriceData
|
|
1637
|
-
: MMOraclePriceData,
|
|
1638
|
-
filterFcn?: DLOBFilterFcn,
|
|
1639
|
-
tickSize?: BN
|
|
1640
|
-
): Generator<DLOBNode> {
|
|
1641
|
-
if (isVariant(marketType, 'spot') && !oraclePriceData) {
|
|
1642
|
-
throw new Error('Must provide OraclePriceData to get spot asks');
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
this.updateRestingLimitOrders(slot);
|
|
1646
|
-
|
|
1647
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
1648
|
-
const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
|
|
1649
|
-
|
|
1650
|
-
if (!nodeLists) {
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
const generatorList = [
|
|
1655
|
-
nodeLists.restingLimit.ask.getGenerator(),
|
|
1656
|
-
nodeLists.floatingLimit.ask.getGenerator(),
|
|
1657
|
-
this.signedMsgGenerator(nodeLists.signedMsg.ask, (x: DLOBNode) =>
|
|
1658
|
-
isRestingLimitOrder(getOrderOrThrow(x), slot)
|
|
1659
|
-
),
|
|
1660
|
-
];
|
|
1661
|
-
|
|
1662
|
-
yield* this.getBestNode(
|
|
1663
|
-
generatorList,
|
|
1664
|
-
oraclePriceData,
|
|
1665
|
-
slot,
|
|
1666
|
-
(bestNode, currentNode, slot, oraclePriceData) => {
|
|
1667
|
-
return bestNode
|
|
1668
|
-
.getPriceOrThrow(oraclePriceData, slot, tickSize)
|
|
1669
|
-
.lt(currentNode.getPriceOrThrow(oraclePriceData, slot, tickSize));
|
|
1670
|
-
},
|
|
1671
|
-
filterFcn
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
/** Same as `getRestingLimitAsks`, but for the bid side (merged best-price-first, highest bid first). */
|
|
1676
|
-
*getRestingLimitBids<T extends MarketType>(
|
|
1677
|
-
marketIndex: number,
|
|
1678
|
-
slot: number,
|
|
1679
|
-
marketType: T,
|
|
1680
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1681
|
-
? OraclePriceData
|
|
1682
|
-
: MMOraclePriceData,
|
|
1683
|
-
filterFcn?: DLOBFilterFcn,
|
|
1684
|
-
tickSize?: BN
|
|
1685
|
-
): Generator<DLOBNode> {
|
|
1686
|
-
if (isVariant(marketType, 'spot') && !oraclePriceData) {
|
|
1687
|
-
throw new Error('Must provide OraclePriceData to get spot bids');
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
this.updateRestingLimitOrders(slot);
|
|
1691
|
-
|
|
1692
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
1693
|
-
const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
|
|
1694
|
-
|
|
1695
|
-
if (!nodeLists) {
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
const generatorList = [
|
|
1700
|
-
nodeLists.restingLimit.bid.getGenerator(),
|
|
1701
|
-
nodeLists.floatingLimit.bid.getGenerator(),
|
|
1702
|
-
this.signedMsgGenerator(nodeLists.signedMsg.bid, (x: DLOBNode) =>
|
|
1703
|
-
isRestingLimitOrder(getOrderOrThrow(x), slot)
|
|
1704
|
-
),
|
|
1705
|
-
];
|
|
1706
|
-
|
|
1707
|
-
yield* this.getBestNode(
|
|
1708
|
-
generatorList,
|
|
1709
|
-
oraclePriceData,
|
|
1710
|
-
slot,
|
|
1711
|
-
(bestNode, currentNode, slot, oraclePriceData) => {
|
|
1712
|
-
return bestNode
|
|
1713
|
-
.getPriceOrThrow(oraclePriceData, slot, tickSize)
|
|
1714
|
-
.gt(currentNode.getPriceOrThrow(oraclePriceData, slot, tickSize));
|
|
1715
|
-
},
|
|
1716
|
-
filterFcn
|
|
1717
|
-
);
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
/**
|
|
1721
|
-
* Merges `getTakingAsks` and `getRestingLimitAsks` into a single best-price-first generator
|
|
1722
|
-
* (ties broken by earliest arrival slot). Nodes with no resolvable price (e.g. still
|
|
1723
|
-
* mid-auction) sort as price `0` — i.e. best — since `getPrice` (not `getPriceOrThrow`) is
|
|
1724
|
-
* used here. Unlike `findTakingNodesToFill`/`findNodesToFill`, this does **not** merge in
|
|
1725
|
-
* fallback (e.g. vAMM) liquidity; the `fallbackAsk` parameter is currently unused/reserved.
|
|
1726
|
-
*
|
|
1727
|
-
* @param marketIndex the market to scan
|
|
1728
|
-
* @param fallbackAsk currently unused
|
|
1729
|
-
* @param slot current slot
|
|
1730
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1731
|
-
* @param oraclePriceData oracle price data for the market; required for spot markets (throws if missing)
|
|
1732
|
-
* @param filterFcn optional predicate; nodes it rejects are skipped
|
|
1733
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
1734
|
-
* @returns a generator of all ask nodes (taking + resting-limit), best price first
|
|
1735
|
-
*/
|
|
1736
|
-
*getAsks<T extends MarketType>(
|
|
1737
|
-
marketIndex: number,
|
|
1738
|
-
_fallbackAsk: BN | undefined,
|
|
1739
|
-
slot: number,
|
|
1740
|
-
marketType: T,
|
|
1741
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1742
|
-
? OraclePriceData
|
|
1743
|
-
: MMOraclePriceData,
|
|
1744
|
-
filterFcn?: DLOBFilterFcn,
|
|
1745
|
-
tickSize?: BN
|
|
1746
|
-
): Generator<DLOBNode> {
|
|
1747
|
-
if (isVariant(marketType, 'spot') && !oraclePriceData) {
|
|
1748
|
-
throw new Error('Must provide OraclePriceData to get spot asks');
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
const generatorList = [
|
|
1752
|
-
this.getTakingAsks(marketIndex, marketType, slot, oraclePriceData),
|
|
1753
|
-
this.getRestingLimitAsks(
|
|
1754
|
-
marketIndex,
|
|
1755
|
-
slot,
|
|
1756
|
-
marketType,
|
|
1757
|
-
oraclePriceData,
|
|
1758
|
-
undefined,
|
|
1759
|
-
tickSize
|
|
1760
|
-
),
|
|
1761
|
-
];
|
|
1762
|
-
|
|
1763
|
-
yield* this.getBestNode(
|
|
1764
|
-
generatorList,
|
|
1765
|
-
oraclePriceData,
|
|
1766
|
-
slot,
|
|
1767
|
-
(bestNode, currentNode, slot, oraclePriceData) => {
|
|
1768
|
-
const bestNodePrice =
|
|
1769
|
-
bestNode.getPrice(oraclePriceData, slot, tickSize) ?? ZERO;
|
|
1770
|
-
const currentNodePrice =
|
|
1771
|
-
currentNode.getPrice(oraclePriceData, slot, tickSize) ?? ZERO;
|
|
1772
|
-
|
|
1773
|
-
if (bestNodePrice.eq(currentNodePrice)) {
|
|
1774
|
-
return getOrderOrThrow(bestNode).slot.lt(
|
|
1775
|
-
getOrderOrThrow(currentNode).slot
|
|
1776
|
-
);
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
return bestNodePrice.lt(currentNodePrice);
|
|
1780
|
-
},
|
|
1781
|
-
filterFcn
|
|
1782
|
-
);
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
/**
|
|
1786
|
-
* Merges `getTakingBids` and `getRestingLimitBids` into a single best-price-first generator
|
|
1787
|
-
* (ties broken by earliest arrival slot). Nodes with no resolvable price sort as `BN_MAX` —
|
|
1788
|
-
* i.e. worst — since a priceless bid shouldn't be preferred over a priced one. Does not merge
|
|
1789
|
-
* in fallback (e.g. vAMM) liquidity; the `fallbackBid` parameter is currently unused/reserved.
|
|
1790
|
-
*
|
|
1791
|
-
* @param marketIndex the market to scan
|
|
1792
|
-
* @param fallbackBid currently unused
|
|
1793
|
-
* @param slot current slot
|
|
1794
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1795
|
-
* @param oraclePriceData oracle price data for the market; required for spot markets (throws if missing)
|
|
1796
|
-
* @param filterFcn optional predicate; nodes it rejects are skipped
|
|
1797
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
1798
|
-
* @returns a generator of all bid nodes (taking + resting-limit), best price first
|
|
1799
|
-
*/
|
|
1800
|
-
*getBids<T extends MarketType>(
|
|
1801
|
-
marketIndex: number,
|
|
1802
|
-
_fallbackBid: BN | undefined,
|
|
1803
|
-
slot: number,
|
|
1804
|
-
marketType: T,
|
|
1805
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1806
|
-
? OraclePriceData
|
|
1807
|
-
: MMOraclePriceData,
|
|
1808
|
-
filterFcn?: DLOBFilterFcn,
|
|
1809
|
-
tickSize?: BN
|
|
1810
|
-
): Generator<DLOBNode> {
|
|
1811
|
-
if (isVariant(marketType, 'spot') && !oraclePriceData) {
|
|
1812
|
-
throw new Error('Must provide OraclePriceData to get spot bids');
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
const generatorList = [
|
|
1816
|
-
this.getTakingBids(marketIndex, marketType, slot, oraclePriceData),
|
|
1817
|
-
this.getRestingLimitBids(
|
|
1818
|
-
marketIndex,
|
|
1819
|
-
slot,
|
|
1820
|
-
marketType,
|
|
1821
|
-
oraclePriceData,
|
|
1822
|
-
undefined,
|
|
1823
|
-
tickSize
|
|
1824
|
-
),
|
|
1825
|
-
];
|
|
1826
|
-
|
|
1827
|
-
yield* this.getBestNode(
|
|
1828
|
-
generatorList,
|
|
1829
|
-
oraclePriceData,
|
|
1830
|
-
slot,
|
|
1831
|
-
(bestNode, currentNode, slot, oraclePriceData) => {
|
|
1832
|
-
const bestNodePrice =
|
|
1833
|
-
bestNode.getPrice(oraclePriceData, slot, tickSize) ?? BN_MAX;
|
|
1834
|
-
const currentNodePrice =
|
|
1835
|
-
currentNode.getPrice(oraclePriceData, slot, tickSize) ?? BN_MAX;
|
|
1836
|
-
|
|
1837
|
-
if (bestNodePrice.eq(currentNodePrice)) {
|
|
1838
|
-
return getOrderOrThrow(bestNode).slot.lt(
|
|
1839
|
-
getOrderOrThrow(currentNode).slot
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
return bestNodePrice.gt(currentNodePrice);
|
|
1844
|
-
},
|
|
1845
|
-
filterFcn
|
|
1846
|
-
);
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
/**
|
|
1850
|
-
* Finds pairs of resting-limit asks and bids that cross each other (`bidPrice >= askPrice`),
|
|
1851
|
-
* assigns maker/taker roles via `determineMakerAndTaker` (post-only orders are always makers;
|
|
1852
|
-
* otherwise whichever order's auction finished later is the taker), and simulates the fill by
|
|
1853
|
-
* updating both orders' `baseAssetAmountFilled` in their `NodeList`s so subsequent iterations
|
|
1854
|
-
* see the reduced remaining size. Same-user matches are skipped. Because both ask and bid
|
|
1855
|
-
* generators are price-sorted, the inner loop `break`s as soon as `bidPrice < askPrice` for a
|
|
1856
|
-
* given ask, since no later (worse) bid can cross either.
|
|
1857
|
-
*
|
|
1858
|
-
* @param marketIndex the market to scan
|
|
1859
|
-
* @param slot current slot
|
|
1860
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
1861
|
-
* @param oraclePriceData oracle price data for the market
|
|
1862
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into all price lookups
|
|
1863
|
-
* @returns `NodeToFill`s (taker node + one maker node each) for every crossing pair found
|
|
1864
|
-
*/
|
|
1865
|
-
findCrossingRestingLimitOrders<T extends MarketType>(
|
|
1866
|
-
marketIndex: number,
|
|
1867
|
-
slot: number,
|
|
1868
|
-
marketType: T,
|
|
1869
|
-
oraclePriceData: T extends { spot: unknown }
|
|
1870
|
-
? OraclePriceData
|
|
1871
|
-
: MMOraclePriceData,
|
|
1872
|
-
tickSize?: BN
|
|
1873
|
-
): NodeToFill[] {
|
|
1874
|
-
const nodesToFill = new Array<NodeToFill>();
|
|
1875
|
-
|
|
1876
|
-
for (const askNode of this.getRestingLimitAsks(
|
|
1877
|
-
marketIndex,
|
|
1878
|
-
slot,
|
|
1879
|
-
marketType,
|
|
1880
|
-
oraclePriceData,
|
|
1881
|
-
undefined,
|
|
1882
|
-
tickSize
|
|
1883
|
-
)) {
|
|
1884
|
-
const bidGenerator = this.getRestingLimitBids(
|
|
1885
|
-
marketIndex,
|
|
1886
|
-
slot,
|
|
1887
|
-
marketType,
|
|
1888
|
-
oraclePriceData,
|
|
1889
|
-
undefined,
|
|
1890
|
-
tickSize
|
|
1891
|
-
);
|
|
1892
|
-
|
|
1893
|
-
for (const bidNode of bidGenerator) {
|
|
1894
|
-
const bidPrice = bidNode.getPriceOrThrow(
|
|
1895
|
-
oraclePriceData,
|
|
1896
|
-
slot,
|
|
1897
|
-
tickSize
|
|
1898
|
-
);
|
|
1899
|
-
const askPrice = askNode.getPriceOrThrow(
|
|
1900
|
-
oraclePriceData,
|
|
1901
|
-
slot,
|
|
1902
|
-
tickSize
|
|
1903
|
-
);
|
|
1904
|
-
|
|
1905
|
-
// orders don't cross
|
|
1906
|
-
if (bidPrice.lt(askPrice)) {
|
|
1907
|
-
break;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
const bidOrder = getOrderOrThrow(bidNode);
|
|
1911
|
-
const askOrder = getOrderOrThrow(askNode);
|
|
1912
|
-
|
|
1913
|
-
// Can't match orders from the same user
|
|
1914
|
-
const sameUser = bidNode.userAccount === askNode.userAccount;
|
|
1915
|
-
if (sameUser) {
|
|
1916
|
-
continue;
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
const makerAndTaker = this.determineMakerAndTaker(askNode, bidNode);
|
|
1920
|
-
|
|
1921
|
-
// unable to match maker and taker due to post only or slot
|
|
1922
|
-
if (!makerAndTaker) {
|
|
1923
|
-
continue;
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
const { takerNode, makerNode } = makerAndTaker;
|
|
1927
|
-
|
|
1928
|
-
const bidBaseRemaining = bidOrder.baseAssetAmount.sub(
|
|
1929
|
-
bidOrder.baseAssetAmountFilled
|
|
1930
|
-
);
|
|
1931
|
-
const askBaseRemaining = askOrder.baseAssetAmount.sub(
|
|
1932
|
-
askOrder.baseAssetAmountFilled
|
|
1933
|
-
);
|
|
1934
|
-
|
|
1935
|
-
const baseFilled = BN.min(bidBaseRemaining, askBaseRemaining);
|
|
1936
|
-
|
|
1937
|
-
const newBidOrder = { ...bidOrder };
|
|
1938
|
-
newBidOrder.baseAssetAmountFilled =
|
|
1939
|
-
bidOrder.baseAssetAmountFilled.add(baseFilled);
|
|
1940
|
-
this.getListForOnChainOrderOrThrow(newBidOrder, slot).update(
|
|
1941
|
-
newBidOrder,
|
|
1942
|
-
getUserAccountOrThrow(bidNode)
|
|
1943
|
-
);
|
|
1944
|
-
|
|
1945
|
-
// ask completely filled
|
|
1946
|
-
const newAskOrder = { ...askOrder };
|
|
1947
|
-
newAskOrder.baseAssetAmountFilled =
|
|
1948
|
-
askOrder.baseAssetAmountFilled.add(baseFilled);
|
|
1949
|
-
this.getListForOnChainOrderOrThrow(newAskOrder, slot).update(
|
|
1950
|
-
newAskOrder,
|
|
1951
|
-
getUserAccountOrThrow(askNode)
|
|
1952
|
-
);
|
|
1953
|
-
|
|
1954
|
-
nodesToFill.push({
|
|
1955
|
-
node: takerNode,
|
|
1956
|
-
makerNodes: [makerNode],
|
|
1957
|
-
});
|
|
1958
|
-
|
|
1959
|
-
if (newAskOrder.baseAssetAmount.eq(newAskOrder.baseAssetAmountFilled)) {
|
|
1960
|
-
break;
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
return nodesToFill;
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
/**
|
|
1969
|
-
* Decides which of a crossing ask/bid pair is the maker and which is the taker: if both are
|
|
1970
|
-
* post-only, they can't be matched (`undefined`); if exactly one is post-only, it's the
|
|
1971
|
-
* maker; otherwise whichever order's auction window (`order.slot + order.auctionDuration`)
|
|
1972
|
-
* ends later is treated as the taker (it "arrived crossing" the earlier order).
|
|
1973
|
-
*
|
|
1974
|
-
* @param askNode the crossing ask node
|
|
1975
|
-
* @param bidNode the crossing bid node
|
|
1976
|
-
* @returns the assigned `{ takerNode, makerNode }`, or `undefined` if both orders are post-only and neither can take
|
|
1977
|
-
*/
|
|
1978
|
-
determineMakerAndTaker(
|
|
1979
|
-
askNode: DLOBNode,
|
|
1980
|
-
bidNode: DLOBNode
|
|
1981
|
-
): { takerNode: DLOBNode; makerNode: DLOBNode } | undefined {
|
|
1982
|
-
const askOrder = getOrderOrThrow(askNode);
|
|
1983
|
-
const bidOrder = getOrderOrThrow(bidNode);
|
|
1984
|
-
const askSlot = askOrder.slot.add(new BN(askOrder.auctionDuration));
|
|
1985
|
-
const bidSlot = bidOrder.slot.add(new BN(bidOrder.auctionDuration));
|
|
1986
|
-
|
|
1987
|
-
if (bidOrder.postOnly && askOrder.postOnly) {
|
|
1988
|
-
return undefined;
|
|
1989
|
-
} else if (bidOrder.postOnly) {
|
|
1990
|
-
return {
|
|
1991
|
-
takerNode: askNode,
|
|
1992
|
-
makerNode: bidNode,
|
|
1993
|
-
};
|
|
1994
|
-
} else if (askOrder.postOnly) {
|
|
1995
|
-
return {
|
|
1996
|
-
takerNode: bidNode,
|
|
1997
|
-
makerNode: askNode,
|
|
1998
|
-
};
|
|
1999
|
-
} else if (askSlot.lte(bidSlot)) {
|
|
2000
|
-
return {
|
|
2001
|
-
takerNode: bidNode,
|
|
2002
|
-
makerNode: askNode,
|
|
2003
|
-
};
|
|
2004
|
-
} else {
|
|
2005
|
-
return {
|
|
2006
|
-
takerNode: askNode,
|
|
2007
|
-
makerNode: bidNode,
|
|
2008
|
-
};
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
/**
|
|
2013
|
-
* Gets the best (lowest) resting-limit ask price for a market. Does not consider fallback
|
|
2014
|
-
* (e.g. vAMM) liquidity.
|
|
2015
|
-
*
|
|
2016
|
-
* @param marketIndex the market to query
|
|
2017
|
-
* @param slot current slot
|
|
2018
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2019
|
-
* @param oraclePriceData oracle price data for the market
|
|
2020
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
2021
|
-
* @returns the best ask price, PRICE_PRECISION (1e6), or `undefined` if there are no resting-limit asks
|
|
2022
|
-
*/
|
|
2023
|
-
public getBestAsk<T extends MarketType>(
|
|
2024
|
-
marketIndex: number,
|
|
2025
|
-
slot: number,
|
|
2026
|
-
marketType: T,
|
|
2027
|
-
oraclePriceData: T extends { spot: unknown }
|
|
2028
|
-
? OraclePriceData
|
|
2029
|
-
: MMOraclePriceData,
|
|
2030
|
-
tickSize?: BN
|
|
2031
|
-
): BN | undefined {
|
|
2032
|
-
const bestAsk = this.getRestingLimitAsks(
|
|
2033
|
-
marketIndex,
|
|
2034
|
-
slot,
|
|
2035
|
-
marketType,
|
|
2036
|
-
oraclePriceData,
|
|
2037
|
-
undefined,
|
|
2038
|
-
tickSize
|
|
2039
|
-
).next().value;
|
|
2040
|
-
|
|
2041
|
-
if (bestAsk) {
|
|
2042
|
-
return bestAsk.getPrice(oraclePriceData, slot, tickSize);
|
|
2043
|
-
}
|
|
2044
|
-
return undefined;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
/**
|
|
2048
|
-
* Gets the best (highest) resting-limit bid price for a market. Does not consider fallback
|
|
2049
|
-
* (e.g. vAMM) liquidity.
|
|
2050
|
-
*
|
|
2051
|
-
* @param marketIndex the market to query
|
|
2052
|
-
* @param slot current slot
|
|
2053
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2054
|
-
* @param oraclePriceData oracle price data for the market
|
|
2055
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
2056
|
-
* @returns the best bid price, PRICE_PRECISION (1e6), or `undefined` if there are no resting-limit bids
|
|
2057
|
-
*/
|
|
2058
|
-
public getBestBid<T extends MarketType>(
|
|
2059
|
-
marketIndex: number,
|
|
2060
|
-
slot: number,
|
|
2061
|
-
marketType: T,
|
|
2062
|
-
oraclePriceData: T extends { spot: unknown }
|
|
2063
|
-
? OraclePriceData
|
|
2064
|
-
: MMOraclePriceData,
|
|
2065
|
-
tickSize?: BN
|
|
2066
|
-
): BN | undefined {
|
|
2067
|
-
const bestBid = this.getRestingLimitBids(
|
|
2068
|
-
marketIndex,
|
|
2069
|
-
slot,
|
|
2070
|
-
marketType,
|
|
2071
|
-
oraclePriceData,
|
|
2072
|
-
undefined,
|
|
2073
|
-
tickSize
|
|
2074
|
-
).next().value;
|
|
2075
|
-
|
|
2076
|
-
if (bestBid) {
|
|
2077
|
-
return bestBid.getPrice(oraclePriceData, slot, tickSize);
|
|
2078
|
-
}
|
|
2079
|
-
return undefined;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
/**
|
|
2083
|
-
* Yields untriggered trigger orders that would close a position in `direction`: for a `long`
|
|
2084
|
-
* position, short-direction orders in the `trigger.below` list (stop triggers on a price
|
|
2085
|
-
* drop); for a `short` position, long-direction orders in `trigger.above` (stop triggers on a
|
|
2086
|
-
* price rise). Includes both `triggerMarket` and `triggerLimit` order types — see
|
|
2087
|
-
* `getStopLossMarkets`/`getStopLossLimits` to filter to one.
|
|
2088
|
-
*
|
|
2089
|
-
* @param marketIndex the market to scan
|
|
2090
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2091
|
-
* @param direction the direction of the position being protected (not the order's own direction)
|
|
2092
|
-
* @returns a generator of stop-loss trigger order nodes
|
|
2093
|
-
*/
|
|
2094
|
-
public *getStopLosses(
|
|
2095
|
-
marketIndex: number,
|
|
2096
|
-
marketType: MarketType,
|
|
2097
|
-
direction: PositionDirection
|
|
2098
|
-
): Generator<DLOBNode> {
|
|
2099
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
2100
|
-
const marketNodeLists = this.getMarketNodeLists(marketTypeStr, marketIndex);
|
|
2101
|
-
|
|
2102
|
-
if (isVariant(direction, 'long') && marketNodeLists.trigger.below) {
|
|
2103
|
-
for (const node of marketNodeLists.trigger.below.getGenerator()) {
|
|
2104
|
-
if (isVariant(getOrderOrThrow(node).direction, 'short')) {
|
|
2105
|
-
yield node;
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
} else if (isVariant(direction, 'short') && marketNodeLists.trigger.above) {
|
|
2109
|
-
for (const node of marketNodeLists.trigger.above.getGenerator()) {
|
|
2110
|
-
if (isVariant(getOrderOrThrow(node).direction, 'long')) {
|
|
2111
|
-
yield node;
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
/** Same as `getStopLosses`, filtered to `triggerMarket` orders only. */
|
|
2118
|
-
public *getStopLossMarkets(
|
|
2119
|
-
marketIndex: number,
|
|
2120
|
-
marketType: MarketType,
|
|
2121
|
-
direction: PositionDirection
|
|
2122
|
-
): Generator<DLOBNode> {
|
|
2123
|
-
for (const node of this.getStopLosses(marketIndex, marketType, direction)) {
|
|
2124
|
-
if (isVariant(getOrderOrThrow(node).orderType, 'triggerMarket')) {
|
|
2125
|
-
yield node;
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
/** Same as `getStopLosses`, filtered to `triggerLimit` orders only. */
|
|
2131
|
-
public *getStopLossLimits(
|
|
2132
|
-
marketIndex: number,
|
|
2133
|
-
marketType: MarketType,
|
|
2134
|
-
direction: PositionDirection
|
|
2135
|
-
): Generator<DLOBNode> {
|
|
2136
|
-
for (const node of this.getStopLosses(marketIndex, marketType, direction)) {
|
|
2137
|
-
if (isVariant(getOrderOrThrow(node).orderType, 'triggerLimit')) {
|
|
2138
|
-
yield node;
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
/**
|
|
2144
|
-
* Yields untriggered trigger orders that would close a position in `direction` for profit:
|
|
2145
|
-
* for a `long` position, short-direction orders in `trigger.above` (take-profit on a price
|
|
2146
|
-
* rise); for a `short` position, long-direction orders in `trigger.below` (take-profit on a
|
|
2147
|
-
* price drop). Includes both `triggerMarket` and `triggerLimit` order types — see
|
|
2148
|
-
* `getTakeProfitMarkets`/`getTakeProfitLimits` to filter to one.
|
|
2149
|
-
*
|
|
2150
|
-
* @param marketIndex the market to scan
|
|
2151
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2152
|
-
* @param direction the direction of the position being protected (not the order's own direction)
|
|
2153
|
-
* @returns a generator of take-profit trigger order nodes
|
|
2154
|
-
*/
|
|
2155
|
-
public *getTakeProfits(
|
|
2156
|
-
marketIndex: number,
|
|
2157
|
-
marketType: MarketType,
|
|
2158
|
-
direction: PositionDirection
|
|
2159
|
-
): Generator<DLOBNode> {
|
|
2160
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
2161
|
-
const marketNodeLists = this.getMarketNodeLists(marketTypeStr, marketIndex);
|
|
2162
|
-
|
|
2163
|
-
if (isVariant(direction, 'long') && marketNodeLists.trigger.above) {
|
|
2164
|
-
for (const node of marketNodeLists.trigger.above.getGenerator()) {
|
|
2165
|
-
if (isVariant(getOrderOrThrow(node).direction, 'short')) {
|
|
2166
|
-
yield node;
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
} else if (isVariant(direction, 'short') && marketNodeLists.trigger.below) {
|
|
2170
|
-
for (const node of marketNodeLists.trigger.below.getGenerator()) {
|
|
2171
|
-
if (isVariant(getOrderOrThrow(node).direction, 'long')) {
|
|
2172
|
-
yield node;
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
/** Same as `getTakeProfits`, filtered to `triggerMarket` orders only. */
|
|
2179
|
-
public *getTakeProfitMarkets(
|
|
2180
|
-
marketIndex: number,
|
|
2181
|
-
marketType: MarketType,
|
|
2182
|
-
direction: PositionDirection
|
|
2183
|
-
): Generator<DLOBNode> {
|
|
2184
|
-
for (const node of this.getTakeProfits(
|
|
2185
|
-
marketIndex,
|
|
2186
|
-
marketType,
|
|
2187
|
-
direction
|
|
2188
|
-
)) {
|
|
2189
|
-
if (isVariant(getOrderOrThrow(node).orderType, 'triggerMarket')) {
|
|
2190
|
-
yield node;
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
/** Same as `getTakeProfits`, filtered to `triggerLimit` orders only. */
|
|
2196
|
-
public *getTakeProfitLimits(
|
|
2197
|
-
marketIndex: number,
|
|
2198
|
-
marketType: MarketType,
|
|
2199
|
-
direction: PositionDirection
|
|
2200
|
-
): Generator<DLOBNode> {
|
|
2201
|
-
for (const node of this.getTakeProfits(
|
|
2202
|
-
marketIndex,
|
|
2203
|
-
marketType,
|
|
2204
|
-
direction
|
|
2205
|
-
)) {
|
|
2206
|
-
if (isVariant(getOrderOrThrow(node).orderType, 'triggerLimit')) {
|
|
2207
|
-
yield node;
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
/**
|
|
2213
|
-
* Finds trigger orders whose condition is now satisfied by `triggerPrice`: `trigger.above`
|
|
2214
|
-
* orders with `triggerPrice > order.triggerPrice`, and `trigger.below` orders with
|
|
2215
|
-
* `triggerPrice < order.triggerPrice`. Both lists are sorted by trigger price with the
|
|
2216
|
-
* nearest-to-triggering order at `head`, so each scan walks from `head` and `break`s at the
|
|
2217
|
-
* first order that isn't (yet) triggered. Returns `[]` immediately if the exchange is paused.
|
|
2218
|
-
*
|
|
2219
|
-
* @param marketIndex the market to scan
|
|
2220
|
-
* @param slot current slot (currently unused by the scan itself, reserved for future use)
|
|
2221
|
-
* @param triggerPrice the price to check trigger conditions against, PRICE_PRECISION (1e6) — typically the current oracle or mark price
|
|
2222
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2223
|
-
* @param stateAccount global protocol state, used for the exchange-pause check
|
|
2224
|
-
* @returns `NodeToTrigger`s for every order ready to be triggered on-chain
|
|
2225
|
-
*/
|
|
2226
|
-
public findNodesToTrigger(
|
|
2227
|
-
marketIndex: number,
|
|
2228
|
-
slot: number,
|
|
2229
|
-
triggerPrice: BN,
|
|
2230
|
-
marketType: MarketType,
|
|
2231
|
-
stateAccount: StateAccount
|
|
2232
|
-
): NodeToTrigger[] {
|
|
2233
|
-
if (exchangePaused(stateAccount)) {
|
|
2234
|
-
return [];
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
const nodesToTrigger: NodeToTrigger[] = [];
|
|
2238
|
-
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
2239
|
-
const marketNodeLists = this.orderLists
|
|
2240
|
-
.get(marketTypeStr)
|
|
2241
|
-
?.get(marketIndex);
|
|
2242
|
-
|
|
2243
|
-
const triggerAboveList = marketNodeLists
|
|
2244
|
-
? marketNodeLists.trigger.above
|
|
2245
|
-
: undefined;
|
|
2246
|
-
if (triggerAboveList) {
|
|
2247
|
-
for (
|
|
2248
|
-
let node = triggerAboveList.head;
|
|
2249
|
-
node !== undefined;
|
|
2250
|
-
node = node.next
|
|
2251
|
-
) {
|
|
2252
|
-
if (triggerPrice.gt(node.order.triggerPrice)) {
|
|
2253
|
-
nodesToTrigger.push({
|
|
2254
|
-
node: node,
|
|
2255
|
-
});
|
|
2256
|
-
} else {
|
|
2257
|
-
break;
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
const triggerBelowList = marketNodeLists
|
|
2263
|
-
? marketNodeLists.trigger.below
|
|
2264
|
-
: undefined;
|
|
2265
|
-
if (triggerBelowList) {
|
|
2266
|
-
for (
|
|
2267
|
-
let node = triggerBelowList.head;
|
|
2268
|
-
node !== undefined;
|
|
2269
|
-
node = node.next
|
|
2270
|
-
) {
|
|
2271
|
-
if (triggerPrice.lt(node.order.triggerPrice)) {
|
|
2272
|
-
nodesToTrigger.push({
|
|
2273
|
-
node: node,
|
|
2274
|
-
});
|
|
2275
|
-
} else {
|
|
2276
|
-
break;
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
return nodesToTrigger;
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
/**
|
|
2285
|
-
* Debug helper: logs the market's best bid, best ask, and mid price (all resting-limit only,
|
|
2286
|
-
* no fallback liquidity), along with each side's spread to the current oracle price, as a
|
|
2287
|
-
* percentage.
|
|
2288
|
-
*
|
|
2289
|
-
* @param velocityClient client used to resolve market accounts and oracle price data
|
|
2290
|
-
* @param slotSubscriber source of the current slot
|
|
2291
|
-
* @param marketIndex the market to print
|
|
2292
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2293
|
-
* @throws if the market currently has no resting-limit bid or ask (best bid/ask is `undefined`)
|
|
2294
|
-
*/
|
|
2295
|
-
public printTop(
|
|
2296
|
-
velocityClient: VelocityClient,
|
|
2297
|
-
slotSubscriber: SlotSubscriber,
|
|
2298
|
-
marketIndex: number,
|
|
2299
|
-
marketType: MarketType
|
|
2300
|
-
) {
|
|
2301
|
-
if (isVariant(marketType, 'perp')) {
|
|
2302
|
-
const slot = slotSubscriber.getSlot();
|
|
2303
|
-
const oraclePriceData =
|
|
2304
|
-
velocityClient.getMMOracleDataForPerpMarket(marketIndex);
|
|
2305
|
-
|
|
2306
|
-
const bestAsk = this.getBestAsk(
|
|
2307
|
-
marketIndex,
|
|
2308
|
-
slot,
|
|
2309
|
-
marketType,
|
|
2310
|
-
oraclePriceData
|
|
2311
|
-
);
|
|
2312
|
-
const bestBid = this.getBestBid(
|
|
2313
|
-
marketIndex,
|
|
2314
|
-
slot,
|
|
2315
|
-
marketType,
|
|
2316
|
-
oraclePriceData
|
|
2317
|
-
);
|
|
2318
|
-
if (bestAsk === undefined || bestBid === undefined) {
|
|
2319
|
-
throw new Error(
|
|
2320
|
-
`printTop requires both a best ask and best bid for market ${marketIndex}`
|
|
2321
|
-
);
|
|
2322
|
-
}
|
|
2323
|
-
const mid = bestAsk.add(bestBid).div(new BN(2));
|
|
2324
|
-
|
|
2325
|
-
const bidSpread =
|
|
2326
|
-
(convertToNumber(bestBid, PRICE_PRECISION) /
|
|
2327
|
-
convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
|
|
2328
|
-
1) *
|
|
2329
|
-
100.0;
|
|
2330
|
-
const askSpread =
|
|
2331
|
-
(convertToNumber(bestAsk, PRICE_PRECISION) /
|
|
2332
|
-
convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
|
|
2333
|
-
1) *
|
|
2334
|
-
100.0;
|
|
2335
|
-
|
|
2336
|
-
const name = decodeName(
|
|
2337
|
-
velocityClient.getPerpMarketAccountOrThrow(marketIndex).name
|
|
2338
|
-
);
|
|
2339
|
-
console.log(`Market ${name} Orders`);
|
|
2340
|
-
console.log(
|
|
2341
|
-
` Ask`,
|
|
2342
|
-
convertToNumber(bestAsk, PRICE_PRECISION).toFixed(3),
|
|
2343
|
-
`(${askSpread.toFixed(4)}%)`
|
|
2344
|
-
);
|
|
2345
|
-
console.log(` Mid`, convertToNumber(mid, PRICE_PRECISION).toFixed(3));
|
|
2346
|
-
console.log(
|
|
2347
|
-
` Bid`,
|
|
2348
|
-
convertToNumber(bestBid, PRICE_PRECISION).toFixed(3),
|
|
2349
|
-
`(${bidSpread.toFixed(4)}%)`
|
|
2350
|
-
);
|
|
2351
|
-
} else if (isVariant(marketType, 'spot')) {
|
|
2352
|
-
const slot = slotSubscriber.getSlot();
|
|
2353
|
-
const oraclePriceData =
|
|
2354
|
-
velocityClient.getOracleDataForSpotMarket(marketIndex);
|
|
2355
|
-
|
|
2356
|
-
const bestAsk = this.getBestAsk(
|
|
2357
|
-
marketIndex,
|
|
2358
|
-
slot,
|
|
2359
|
-
MarketType.SPOT,
|
|
2360
|
-
oraclePriceData
|
|
2361
|
-
);
|
|
2362
|
-
const bestBid = this.getBestBid(
|
|
2363
|
-
marketIndex,
|
|
2364
|
-
slot,
|
|
2365
|
-
MarketType.SPOT,
|
|
2366
|
-
oraclePriceData
|
|
2367
|
-
);
|
|
2368
|
-
if (bestAsk === undefined || bestBid === undefined) {
|
|
2369
|
-
throw new Error(
|
|
2370
|
-
`printTop requires both a best ask and best bid for market ${marketIndex}`
|
|
2371
|
-
);
|
|
2372
|
-
}
|
|
2373
|
-
const mid = bestAsk.add(bestBid).div(new BN(2));
|
|
2374
|
-
|
|
2375
|
-
const bidSpread =
|
|
2376
|
-
(convertToNumber(bestBid, PRICE_PRECISION) /
|
|
2377
|
-
convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
|
|
2378
|
-
1) *
|
|
2379
|
-
100.0;
|
|
2380
|
-
const askSpread =
|
|
2381
|
-
(convertToNumber(bestAsk, PRICE_PRECISION) /
|
|
2382
|
-
convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
|
|
2383
|
-
1) *
|
|
2384
|
-
100.0;
|
|
2385
|
-
|
|
2386
|
-
const name = decodeName(
|
|
2387
|
-
velocityClient.getSpotMarketAccountOrThrow(marketIndex).name
|
|
2388
|
-
);
|
|
2389
|
-
console.log(`Market ${name} Orders`);
|
|
2390
|
-
console.log(
|
|
2391
|
-
` Ask`,
|
|
2392
|
-
convertToNumber(bestAsk, PRICE_PRECISION).toFixed(3),
|
|
2393
|
-
`(${askSpread.toFixed(4)}%)`
|
|
2394
|
-
);
|
|
2395
|
-
console.log(` Mid`, convertToNumber(mid, PRICE_PRECISION).toFixed(3));
|
|
2396
|
-
console.log(
|
|
2397
|
-
` Bid`,
|
|
2398
|
-
convertToNumber(bestBid, PRICE_PRECISION).toFixed(3),
|
|
2399
|
-
`(${bidSpread.toFixed(4)}%)`
|
|
2400
|
-
);
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
/** Flattens every order across every `NodeList` (perp and spot, all categories/sides) into a single `DLOBOrders` array of `{ user, order }` pairs, in no particular cross-list order. */
|
|
2405
|
-
public getDLOBOrders(): DLOBOrders {
|
|
2406
|
-
const dlobOrders: DLOBOrders = [];
|
|
2407
|
-
|
|
2408
|
-
for (const nodeList of this.getNodeLists()) {
|
|
2409
|
-
for (const node of nodeList.getGenerator()) {
|
|
2410
|
-
dlobOrders.push({
|
|
2411
|
-
user: new PublicKey(getUserAccountOrThrow(node)),
|
|
2412
|
-
order: getOrderOrThrow(node),
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
return dlobOrders;
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
/** Yields every `NodeList` (all ten category/side combinations, per market) across every perp market, then every spot market. Used by `getOrder`/`getDLOBOrders` to walk the entire book. */
|
|
2421
|
-
*getNodeLists(): Generator<NodeList<DLOBNodeType>> {
|
|
2422
|
-
for (const [_, nodeLists] of this.getOrderListsForMarketType('perp')) {
|
|
2423
|
-
yield nodeLists.restingLimit.bid;
|
|
2424
|
-
yield nodeLists.restingLimit.ask;
|
|
2425
|
-
yield nodeLists.takingLimit.bid;
|
|
2426
|
-
yield nodeLists.takingLimit.ask;
|
|
2427
|
-
yield nodeLists.market.bid;
|
|
2428
|
-
yield nodeLists.market.ask;
|
|
2429
|
-
yield nodeLists.floatingLimit.bid;
|
|
2430
|
-
yield nodeLists.floatingLimit.ask;
|
|
2431
|
-
yield nodeLists.trigger.above;
|
|
2432
|
-
yield nodeLists.trigger.below;
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
for (const [_, nodeLists] of this.getOrderListsForMarketType('spot')) {
|
|
2436
|
-
yield nodeLists.restingLimit.bid;
|
|
2437
|
-
yield nodeLists.restingLimit.ask;
|
|
2438
|
-
yield nodeLists.takingLimit.bid;
|
|
2439
|
-
yield nodeLists.takingLimit.ask;
|
|
2440
|
-
yield nodeLists.market.bid;
|
|
2441
|
-
yield nodeLists.market.ask;
|
|
2442
|
-
yield nodeLists.floatingLimit.bid;
|
|
2443
|
-
yield nodeLists.floatingLimit.ask;
|
|
2444
|
-
yield nodeLists.trigger.above;
|
|
2445
|
-
yield nodeLists.trigger.below;
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
/**
|
|
2450
|
-
* Get an L2 (aggregated price/size) view of the order book for a given market: resting-limit
|
|
2451
|
-
* DLOB liquidity merged with any supplied fallback generators (e.g. the vAMM, via
|
|
2452
|
-
* `getVammL2Generator`), then bucketed into up to `depth` levels per side via `createL2Levels`.
|
|
2453
|
-
* Does not include taking (still-auctioning) orders — only resting-limit makers and fallback
|
|
2454
|
-
* liquidity are represented.
|
|
2455
|
-
*
|
|
2456
|
-
* @param marketIndex the market to build a book for
|
|
2457
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2458
|
-
* @param slot current slot, used to resolve resting-limit order prices
|
|
2459
|
-
* @param oraclePriceData oracle price data for the market
|
|
2460
|
-
* @param depth how many price levels of the order book to return, per side
|
|
2461
|
-
* @param fallbackL2Generators additional non-DLOB liquidity sources to merge in, e.g. `getVammL2Generator`'s output; defaults to `[]`
|
|
2462
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into all DLOB price lookups; defaults to no rounding if omitted
|
|
2463
|
-
* @returns the merged `L2OrderBook`, tagged with the given `slot`
|
|
2464
|
-
*/
|
|
2465
|
-
public getL2<T extends MarketType>({
|
|
2466
|
-
marketIndex,
|
|
2467
|
-
marketType,
|
|
2468
|
-
slot,
|
|
2469
|
-
oraclePriceData,
|
|
2470
|
-
depth,
|
|
2471
|
-
fallbackL2Generators = [],
|
|
2472
|
-
tickSize,
|
|
2473
|
-
}: {
|
|
2474
|
-
marketIndex: number;
|
|
2475
|
-
marketType: T;
|
|
2476
|
-
slot: number;
|
|
2477
|
-
oraclePriceData: T extends { spot: unknown }
|
|
2478
|
-
? OraclePriceData
|
|
2479
|
-
: MMOraclePriceData;
|
|
2480
|
-
depth: number;
|
|
2481
|
-
fallbackL2Generators?: L2OrderBookGenerator[];
|
|
2482
|
-
tickSize?: BN;
|
|
2483
|
-
}): L2OrderBook {
|
|
2484
|
-
const makerAskL2LevelGenerator = getL2GeneratorFromDLOBNodes(
|
|
2485
|
-
this.getRestingLimitAsks(
|
|
2486
|
-
marketIndex,
|
|
2487
|
-
slot,
|
|
2488
|
-
marketType,
|
|
2489
|
-
oraclePriceData,
|
|
2490
|
-
undefined,
|
|
2491
|
-
tickSize
|
|
2492
|
-
),
|
|
2493
|
-
oraclePriceData,
|
|
2494
|
-
slot,
|
|
2495
|
-
tickSize
|
|
2496
|
-
);
|
|
2497
|
-
|
|
2498
|
-
const fallbackAskGenerators = fallbackL2Generators.map(
|
|
2499
|
-
(fallbackL2Generator) => {
|
|
2500
|
-
return fallbackL2Generator.getL2Asks();
|
|
2501
|
-
}
|
|
2502
|
-
);
|
|
2503
|
-
|
|
2504
|
-
const askL2LevelGenerator = mergeL2LevelGenerators(
|
|
2505
|
-
[makerAskL2LevelGenerator, ...fallbackAskGenerators],
|
|
2506
|
-
(a, b) => {
|
|
2507
|
-
return a.price.lt(b.price);
|
|
2508
|
-
}
|
|
2509
|
-
);
|
|
2510
|
-
|
|
2511
|
-
const asks = createL2Levels(askL2LevelGenerator, depth);
|
|
2512
|
-
|
|
2513
|
-
const makerBidGenerator = getL2GeneratorFromDLOBNodes(
|
|
2514
|
-
this.getRestingLimitBids(
|
|
2515
|
-
marketIndex,
|
|
2516
|
-
slot,
|
|
2517
|
-
marketType,
|
|
2518
|
-
oraclePriceData,
|
|
2519
|
-
undefined,
|
|
2520
|
-
tickSize
|
|
2521
|
-
),
|
|
2522
|
-
oraclePriceData,
|
|
2523
|
-
slot,
|
|
2524
|
-
tickSize
|
|
2525
|
-
);
|
|
2526
|
-
|
|
2527
|
-
const fallbackBidGenerators = fallbackL2Generators.map((fallbackOrders) => {
|
|
2528
|
-
return fallbackOrders.getL2Bids();
|
|
2529
|
-
});
|
|
2530
|
-
|
|
2531
|
-
const bidL2LevelGenerator = mergeL2LevelGenerators(
|
|
2532
|
-
[makerBidGenerator, ...fallbackBidGenerators],
|
|
2533
|
-
(a, b) => {
|
|
2534
|
-
return a.price.gt(b.price);
|
|
2535
|
-
}
|
|
2536
|
-
);
|
|
2537
|
-
|
|
2538
|
-
const bids = createL2Levels(bidL2LevelGenerator, depth);
|
|
2539
|
-
|
|
2540
|
-
return {
|
|
2541
|
-
bids,
|
|
2542
|
-
asks,
|
|
2543
|
-
slot,
|
|
2544
|
-
};
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
/**
|
|
2548
|
-
* Get an L3 (individual resting order) view of the order book for a given market. Only
|
|
2549
|
-
* resting-limit orders are included — no taking orders and no fallback (e.g. vAMM) liquidity.
|
|
2550
|
-
*
|
|
2551
|
-
* @param marketIndex the market to build a book for
|
|
2552
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2553
|
-
* @param slot current slot, used to resolve resting-limit order prices
|
|
2554
|
-
* @param oraclePriceData oracle price data for the market
|
|
2555
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
2556
|
-
* @returns the `L3OrderBook`, tagged with the given `slot`
|
|
2557
|
-
*/
|
|
2558
|
-
public getL3<T extends MarketType>({
|
|
2559
|
-
marketIndex,
|
|
2560
|
-
marketType,
|
|
2561
|
-
slot,
|
|
2562
|
-
oraclePriceData,
|
|
2563
|
-
tickSize,
|
|
2564
|
-
}: {
|
|
2565
|
-
marketIndex: number;
|
|
2566
|
-
marketType: T;
|
|
2567
|
-
slot: number;
|
|
2568
|
-
oraclePriceData: T extends { spot: unknown }
|
|
2569
|
-
? OraclePriceData
|
|
2570
|
-
: MMOraclePriceData;
|
|
2571
|
-
tickSize?: BN;
|
|
2572
|
-
}): L3OrderBook {
|
|
2573
|
-
const bids: L3Level[] = [];
|
|
2574
|
-
const asks: L3Level[] = [];
|
|
2575
|
-
|
|
2576
|
-
const restingAsks = this.getRestingLimitAsks(
|
|
2577
|
-
marketIndex,
|
|
2578
|
-
slot,
|
|
2579
|
-
marketType,
|
|
2580
|
-
oraclePriceData,
|
|
2581
|
-
undefined,
|
|
2582
|
-
tickSize
|
|
2583
|
-
);
|
|
2584
|
-
|
|
2585
|
-
for (const ask of restingAsks) {
|
|
2586
|
-
const askOrder = getOrderOrThrow(ask);
|
|
2587
|
-
asks.push({
|
|
2588
|
-
price: ask.getPriceOrThrow(oraclePriceData, slot, tickSize),
|
|
2589
|
-
size: askOrder.baseAssetAmount.sub(askOrder.baseAssetAmountFilled),
|
|
2590
|
-
maker: new PublicKey(getUserAccountOrThrow(ask)),
|
|
2591
|
-
orderId: askOrder.orderId,
|
|
2592
|
-
});
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
const restingBids = this.getRestingLimitBids(
|
|
2596
|
-
marketIndex,
|
|
2597
|
-
slot,
|
|
2598
|
-
marketType,
|
|
2599
|
-
oraclePriceData,
|
|
2600
|
-
undefined,
|
|
2601
|
-
tickSize
|
|
2602
|
-
);
|
|
2603
|
-
|
|
2604
|
-
for (const bid of restingBids) {
|
|
2605
|
-
const bidOrder = getOrderOrThrow(bid);
|
|
2606
|
-
bids.push({
|
|
2607
|
-
price: bid.getPriceOrThrow(oraclePriceData, slot, tickSize),
|
|
2608
|
-
size: bidOrder.baseAssetAmount.sub(bidOrder.baseAssetAmountFilled),
|
|
2609
|
-
maker: new PublicKey(getUserAccountOrThrow(bid)),
|
|
2610
|
-
orderId: bidOrder.orderId,
|
|
2611
|
-
});
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
return {
|
|
2615
|
-
bids,
|
|
2616
|
-
asks,
|
|
2617
|
-
slot,
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
private estimateFillExactBaseAmountInForSide(
|
|
2622
|
-
baseAmountIn: BN,
|
|
2623
|
-
oraclePriceData: OraclePriceData,
|
|
2624
|
-
slot: number,
|
|
2625
|
-
dlobSide: Generator<DLOBNode>,
|
|
2626
|
-
tickSize?: BN
|
|
2627
|
-
): BN {
|
|
2628
|
-
let runningSumQuote = ZERO;
|
|
2629
|
-
let runningSumBase = ZERO;
|
|
2630
|
-
for (const side of dlobSide) {
|
|
2631
|
-
const price = side.getPriceOrThrow(oraclePriceData, slot, tickSize); //side.order.quoteAssetAmount.div(side.order.baseAssetAmount);
|
|
2632
|
-
const sideOrder = getOrderOrThrow(side);
|
|
2633
|
-
const baseAmountRemaining = sideOrder.baseAssetAmount.sub(
|
|
2634
|
-
sideOrder.baseAssetAmountFilled
|
|
2635
|
-
);
|
|
2636
|
-
if (runningSumBase.add(baseAmountRemaining).gt(baseAmountIn)) {
|
|
2637
|
-
const remainingBase = baseAmountIn.sub(runningSumBase);
|
|
2638
|
-
runningSumBase = runningSumBase.add(remainingBase);
|
|
2639
|
-
runningSumQuote = runningSumQuote.add(remainingBase.mul(price));
|
|
2640
|
-
break;
|
|
2641
|
-
} else {
|
|
2642
|
-
runningSumBase = runningSumBase.add(baseAmountRemaining);
|
|
2643
|
-
runningSumQuote = runningSumQuote.add(baseAmountRemaining.mul(price));
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
return runningSumQuote
|
|
2648
|
-
.mul(QUOTE_PRECISION)
|
|
2649
|
-
.div(BASE_PRECISION.mul(PRICE_PRECISION));
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
/**
|
|
2653
|
-
* Estimates the quote amount that would be filled for a given base amount, walking
|
|
2654
|
-
* resting-limit asks (for a `long`/buy) or bids (for a `short`/sell) from best price outward
|
|
2655
|
-
* and summing `price * size` until `baseAmount` is consumed. Does not include fallback (e.g.
|
|
2656
|
-
* vAMM) liquidity or taking orders, and does not mutate any order state — this is a read-only
|
|
2657
|
-
* estimate, not a simulated fill.
|
|
2658
|
-
*
|
|
2659
|
-
* @param marketIndex the market to estimate against
|
|
2660
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2661
|
-
* @param baseAmount the base amount to fill, BASE_PRECISION (1e9)
|
|
2662
|
-
* @param orderDirection direction of the hypothetical taker order (`long` walks asks, `short` walks bids)
|
|
2663
|
-
* @param slot current slot, used to resolve resting-limit order prices
|
|
2664
|
-
* @param oraclePriceData oracle price data for the market
|
|
2665
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
2666
|
-
* @returns the estimated quote amount filled, QUOTE_PRECISION (1e6); if resting liquidity is thinner than `baseAmount`, the result silently reflects only the liquidity that was actually walked (no error/flag for partial fill), and is `0` if there is no resting liquidity on that side at all
|
|
2667
|
-
* @throws if `orderDirection` is neither `long` nor `short`
|
|
2668
|
-
*/
|
|
2669
|
-
public estimateFillWithExactBaseAmount<T extends MarketType>({
|
|
2670
|
-
marketIndex,
|
|
2671
|
-
marketType,
|
|
2672
|
-
baseAmount,
|
|
2673
|
-
orderDirection,
|
|
2674
|
-
slot,
|
|
2675
|
-
oraclePriceData,
|
|
2676
|
-
tickSize,
|
|
2677
|
-
}: {
|
|
2678
|
-
marketIndex: number;
|
|
2679
|
-
marketType: T;
|
|
2680
|
-
baseAmount: BN;
|
|
2681
|
-
orderDirection: PositionDirection;
|
|
2682
|
-
slot: number;
|
|
2683
|
-
oraclePriceData: T extends { spot: unknown }
|
|
2684
|
-
? OraclePriceData
|
|
2685
|
-
: MMOraclePriceData;
|
|
2686
|
-
tickSize?: BN;
|
|
2687
|
-
}): BN {
|
|
2688
|
-
if (isVariant(orderDirection, 'long')) {
|
|
2689
|
-
return this.estimateFillExactBaseAmountInForSide(
|
|
2690
|
-
baseAmount,
|
|
2691
|
-
oraclePriceData,
|
|
2692
|
-
slot,
|
|
2693
|
-
this.getRestingLimitAsks(
|
|
2694
|
-
marketIndex,
|
|
2695
|
-
slot,
|
|
2696
|
-
marketType,
|
|
2697
|
-
oraclePriceData,
|
|
2698
|
-
undefined,
|
|
2699
|
-
tickSize
|
|
2700
|
-
),
|
|
2701
|
-
tickSize
|
|
2702
|
-
);
|
|
2703
|
-
} else if (isVariant(orderDirection, 'short')) {
|
|
2704
|
-
return this.estimateFillExactBaseAmountInForSide(
|
|
2705
|
-
baseAmount,
|
|
2706
|
-
oraclePriceData,
|
|
2707
|
-
slot,
|
|
2708
|
-
this.getRestingLimitBids(
|
|
2709
|
-
marketIndex,
|
|
2710
|
-
slot,
|
|
2711
|
-
marketType,
|
|
2712
|
-
oraclePriceData,
|
|
2713
|
-
undefined,
|
|
2714
|
-
tickSize
|
|
2715
|
-
),
|
|
2716
|
-
tickSize
|
|
2717
|
-
);
|
|
2718
|
-
}
|
|
2719
|
-
throw new Error(
|
|
2720
|
-
`Invalid order direction ${getVariant(
|
|
2721
|
-
orderDirection
|
|
2722
|
-
)}: must be long or short`
|
|
2723
|
-
);
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
/**
|
|
2727
|
-
* Collects the pubkeys of up to `numMakers` distinct makers currently resting best-priced on
|
|
2728
|
-
* one side of the book — bids for a `long` taker, asks for a `short` taker — in best-price
|
|
2729
|
-
* order. Used to pick candidate maker accounts to pass as `remaining_accounts` when
|
|
2730
|
-
* submitting a fill instruction. A maker with multiple resting orders at different prices
|
|
2731
|
-
* only counts once toward `numMakers`.
|
|
2732
|
-
*
|
|
2733
|
-
* @param marketIndex the market to scan
|
|
2734
|
-
* @param marketType `MarketType.PERP` or `MarketType.SPOT`
|
|
2735
|
-
* @param direction direction of the taker order being matched (`long` collects bid-side makers, `short` collects ask-side makers)
|
|
2736
|
-
* @param slot current slot, used to resolve resting-limit order prices
|
|
2737
|
-
* @param oraclePriceData oracle price data for the market
|
|
2738
|
-
* @param numMakers maximum number of distinct maker pubkeys to return
|
|
2739
|
-
* @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
|
|
2740
|
-
* @returns up to `numMakers` distinct maker `PublicKey`s, best price first
|
|
2741
|
-
*/
|
|
2742
|
-
public getBestMakers<T extends MarketType>({
|
|
2743
|
-
marketIndex,
|
|
2744
|
-
marketType,
|
|
2745
|
-
direction,
|
|
2746
|
-
slot,
|
|
2747
|
-
oraclePriceData,
|
|
2748
|
-
numMakers,
|
|
2749
|
-
tickSize,
|
|
2750
|
-
}: {
|
|
2751
|
-
marketIndex: number;
|
|
2752
|
-
marketType: T;
|
|
2753
|
-
direction: PositionDirection;
|
|
2754
|
-
slot: number;
|
|
2755
|
-
oraclePriceData: T extends { spot: unknown }
|
|
2756
|
-
? OraclePriceData
|
|
2757
|
-
: MMOraclePriceData;
|
|
2758
|
-
numMakers: number;
|
|
2759
|
-
tickSize?: BN;
|
|
2760
|
-
}): PublicKey[] {
|
|
2761
|
-
const makers = new Map<string, PublicKey>();
|
|
2762
|
-
const generator = isVariant(direction, 'long')
|
|
2763
|
-
? this.getRestingLimitBids(
|
|
2764
|
-
marketIndex,
|
|
2765
|
-
slot,
|
|
2766
|
-
marketType,
|
|
2767
|
-
oraclePriceData,
|
|
2768
|
-
undefined,
|
|
2769
|
-
tickSize
|
|
2770
|
-
)
|
|
2771
|
-
: this.getRestingLimitAsks(
|
|
2772
|
-
marketIndex,
|
|
2773
|
-
slot,
|
|
2774
|
-
marketType,
|
|
2775
|
-
oraclePriceData,
|
|
2776
|
-
undefined,
|
|
2777
|
-
tickSize
|
|
2778
|
-
);
|
|
2779
|
-
|
|
2780
|
-
for (const node of generator) {
|
|
2781
|
-
const userAccount = getUserAccountOrThrow(node);
|
|
2782
|
-
if (!makers.has(userAccount)) {
|
|
2783
|
-
makers.set(userAccount, new PublicKey(userAccount));
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
if (makers.size === numMakers) {
|
|
2787
|
-
break;
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
return Array.from(makers.values());
|
|
2792
|
-
}
|
|
2793
|
-
}
|