Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix vault orderbook flickering #1638

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/indexer-build-and-push-dev-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on: # yamllint disable-line rule:truthy
- main
- 'release/indexer/v[0-9]+.[0-9]+.x' # e.g. release/indexer/v0.1.x
- 'release/indexer/v[0-9]+.x' # e.g. release/indexer/v1.x
- 'chenyao/fix-vault-orderbook-flickering'
# TODO(DEC-837): Customize github build and push to ECR by service with paths

jobs:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/protocol-build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on: # yamllint disable-line rule:truthy
- main
- 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x
- 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x
- 'chenyao/fix-vault-orderbook-flickering'

jobs:
build-and-push-dev:
Expand Down
2 changes: 1 addition & 1 deletion indexer/packages/redis/src/scripts/place_order.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ else
-- to "false".
redis.call("set", orderKey, newOrder)
-- refer to the above comment on order data format
redis.call("set", orderDataKey, newOrderExpiry .. "_" .. oldTotalFilledQuantums .. "_false")
redis.call("set", orderDataKey, newOrderExpiry .. "_" .. oldTotalFilledQuantums .. "_true")
-- Long-term orders will be on-chain, so we only need to store expiry data for short-term orders
if isShortTermOrder then
-- The expiry is guaranteed to be different, so overwrite the old one from the expiry cache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,14 @@ describe('order-place-handler', () => {
) => {
const oldOrderTotalFilled: number = 10;
const oldPriceLevelInitialQuantums: number = Number(initialOrderToPlace.quantums) * 2;
// After replacing the order the quantums at the price level of the old order should be:
// initial quantums - (old order quantums - old order total filled)
const expectedPriceLevelQuantums: number = (
oldPriceLevelInitialQuantums - (Number(initialOrderToPlace.quantums) - oldOrderTotalFilled)
);
const expectedPriceLevel: PriceLevel = {
humanPrice: expectedRedisOrder.price,
quantums: oldPriceLevelInitialQuantums.toString(),
quantums: expectedPriceLevelQuantums.toString(),
Comment on lines +539 to +546
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor to simplify the calculation of expectedPriceLevelQuantums.

The calculation of expectedPriceLevelQuantums can be simplified for better readability. Consider using a helper function to encapsulate the logic, which can also be reused in other tests if needed.

- const expectedPriceLevelQuantums: number = (
-   oldPriceLevelInitialQuantums - (Number(initialOrderToPlace.quantums) - oldOrderTotalFilled)
- );
+ const expectedPriceLevelQuantums: number = calculateNetQuantums(oldPriceLevelInitialQuantums, initialOrderToPlace.quantums, oldOrderTotalFilled);

+ function calculateNetQuantums(initialQuantums, orderQuantums, totalFilled) {
+   return initialQuantums - (Number(orderQuantums) - totalFilled);
+ }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
// After replacing the order the quantums at the price level of the old order should be:
// initial quantums - (old order quantums - old order total filled)
const expectedPriceLevelQuantums: number = (
oldPriceLevelInitialQuantums - (Number(initialOrderToPlace.quantums) - oldOrderTotalFilled)
);
const expectedPriceLevel: PriceLevel = {
humanPrice: expectedRedisOrder.price,
quantums: oldPriceLevelInitialQuantums.toString(),
quantums: expectedPriceLevelQuantums.toString(),
// After replacing the order the quantums at the price level of the old order should be:
// initial quantums - (old order quantums - old order total filled)
const expectedPriceLevelQuantums: number = calculateNetQuantums(oldPriceLevelInitialQuantums, initialOrderToPlace.quantums, oldOrderTotalFilled);
function calculateNetQuantums(initialQuantums, orderQuantums, totalFilled) {
return initialQuantums - (Number(orderQuantums) - totalFilled);
}
const expectedPriceLevel: PriceLevel = {
humanPrice: expectedRedisOrder.price,
quantums: expectedPriceLevelQuantums.toString(),

lastUpdated: expect.stringMatching(/^[0-9]{10}$/),
};

Expand Down Expand Up @@ -1082,6 +1087,11 @@ describe('order-place-handler', () => {
APIOrderStatusEnum.BEST_EFFORT_OPENED,
true,
);

expect(logger.info).toHaveBeenCalledWith(expect.objectContaining({
at: 'OrderPlaceHandler#handle',
message: 'Total filled of order in Redis exceeds order quantums.',
}));
});
});
});
Expand Down
87 changes: 87 additions & 0 deletions indexer/services/vulcan/src/handlers/order-place-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ import {
OrderTable,
PerpetualMarketFromDatabase,
perpetualMarketRefresher,
protocolTranslations,
} from '@dydxprotocol-indexer/postgres';
import {
CanceledOrdersCache,
placeOrder,
PlaceOrderResult,
StatefulOrderUpdatesCache,
convertToRedisOrder,
OrderbookLevelsCache,
} from '@dydxprotocol-indexer/redis';
import {
getOrderIdHash,
isLongTermOrder,
isStatefulOrder,
ORDER_FLAG_SHORT_TERM,
requiresImmediateExecution,
} from '@dydxprotocol-indexer/v4-proto-parser';
import {
IndexerOrder,
Expand All @@ -28,6 +31,7 @@ import {
OrderUpdateV1,
RedisOrder,
} from '@dydxprotocol-indexer/v4-protos';
import Big from 'big.js';
import { IHeaders, Message } from 'kafkajs';

import config from '../config';
Expand Down Expand Up @@ -96,6 +100,12 @@ export class OrderPlaceHandler extends Handler {
stats.increment(`${config.SERVICE_NAME}.place_order_handler.replaced_order`, 1);
}

await this.updatePriceLevel(
placeOrderResult,
perpetualMarket,
update,
);

// TODO(CLOB-597): Remove this logic and log erorrs once best-effort-open is not sent for
// stateful orders in the protocol
if (this.shouldSendSubaccountMessage(
Expand Down Expand Up @@ -158,6 +168,83 @@ export class OrderPlaceHandler extends Handler {
}
}

/**
* Updates the price level given the result of calling `placeOrder`.
* @param result `PlaceOrderResult` from calling `placeOrder`
* @param perpetualMarket Perpetual market object corresponding to the perpetual market of the
* order
* @param update Off-chain update
* @returns
*/
// eslint-disable-next-line @typescript-eslint/require-await
protected async updatePriceLevel(
result: PlaceOrderResult,
perpetualMarket: PerpetualMarketFromDatabase,
update: OffChainUpdateV1,
): Promise<number | undefined> {
// TODO(DEC-1339): Update price levels based on if the order is reduce-only and if the replaced
// order is reduce-only.
if (
result.replaced !== true ||
result.restingOnBook !== true ||
requiresImmediateExecution(result.oldOrder!.order!.timeInForce)
) {
return undefined;
}

const remainingSizeDeltaInQuantums: Big = this.getRemainingSizeDeltaInQuantums(result);

if (remainingSizeDeltaInQuantums.eq(0)) {
// No update to the price level if remaining quantums = 0
// An order could have remaining quantums = 0 intra-block, as an order is only considered
// filled once the fills are committed in a block
return undefined;
}

if (remainingSizeDeltaInQuantums.lt(0)) {
// Log error and skip updating orderbook levels if old order had negative remaining
// quantums
logger.info({
at: 'OrderPlaceHandler#handle',
message: 'Total filled of order in Redis exceeds order quantums.',
placeOrderResult: result,
update,
});
stats.increment(`${config.SERVICE_NAME}.order_place_total_filled_exceeds_size`, 1);
return undefined;
}

// If the remaining size is not equal or less than 0, it must be greater than 0.
// Remove the remaining size of the replaced order from the orderbook, by decrementing
// the total size of orders at the price of the replaced order
return runFuncWithTimingStat(
OrderbookLevelsCache.updatePriceLevel({
ticker: perpetualMarket.ticker,
side: protocolTranslations.protocolOrderSideToOrderSide(result.oldOrder!.order!.side),
humanPrice: result.oldOrder!.price,
// Delta should be -1 * remaining size of order in quantums and an integer
sizeDeltaInQuantums: remainingSizeDeltaInQuantums.mul(-1).toFixed(0),
client: redisClient,
}),
this.generateTimingStatsOptions('update_price_level'),
);
}

/**
* Gets the remaining size of the old order if the order was replaced.
* @param result Result of placing an order, should be for a replaced order so both `oldOrder` and
* `oldTotalFilledQuantums` properties should exist on the place order result.
* @returns Remaining size of the old order that was replaced.
*/
protected getRemainingSizeDeltaInQuantums(result: PlaceOrderResult): Big {
const sizeDeltaInQuantums: Big = Big(
result.oldOrder!.order!.quantums.toString(),
).minus(
result.oldTotalFilledQuantums!,
);
return sizeDeltaInQuantums;
}

/**
* Determine whether to send a subaccount websocket message given the order place.
* @param orderPlace
Expand Down
1 change: 1 addition & 0 deletions protocol/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,7 @@ func New(
app.PricesKeeper,
app.SendingKeeper,
app.SubaccountsKeeper,
app.IndexerEventManager,
[]string{
lib.GovModuleAddress.String(),
delaymsgmoduletypes.ModuleAddress.String(),
Expand Down
8 changes: 4 additions & 4 deletions protocol/mocks/ClobKeeper.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions protocol/testutil/keeper/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func createVaultKeeper(
&mocks.PricesKeeper{},
&mocks.SendingKeeper{},
&mocks.SubaccountsKeeper{},
&mocks.IndexerEventManager{},
[]string{
lib.GovModuleAddress.String(),
delaymsgtypes.ModuleAddress.String(),
Expand Down
25 changes: 14 additions & 11 deletions protocol/x/clob/keeper/msg_server_cancel_orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (k msgServer) CancelOrder(
) (resp *types.MsgCancelOrderResponse, err error) {
ctx := lib.UnwrapSDKContext(goCtx, types.ModuleName)

if err := k.Keeper.HandleMsgCancelOrder(ctx, msg); err != nil {
if err := k.Keeper.HandleMsgCancelOrder(ctx, msg, false); err != nil {
return nil, err
}

Expand All @@ -37,6 +37,7 @@ func (k msgServer) CancelOrder(
func (k Keeper) HandleMsgCancelOrder(
ctx sdk.Context,
msg *types.MsgCancelOrder,
isInternalOrder bool,
) (err error) {
lib.AssertDeliverTxMode(ctx)

Expand Down Expand Up @@ -111,17 +112,19 @@ func (k Keeper) HandleMsgCancelOrder(
k.MustSetProcessProposerMatchesEvents(ctx, processProposerMatchesEvents)

// 4. Add the relevant on-chain Indexer event for the cancellation.
k.GetIndexerEventManager().AddTxnEvent(
ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewStatefulOrderRemovalEvent(
msg.OrderId,
indexershared.OrderRemovalReason_ORDER_REMOVAL_REASON_USER_CANCELED,
if !isInternalOrder {
k.GetIndexerEventManager().AddTxnEvent(
ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewStatefulOrderRemovalEvent(
msg.OrderId,
indexershared.OrderRemovalReason_ORDER_REMOVAL_REASON_USER_CANCELED,
),
),
),
)
)
}

return nil
}
56 changes: 31 additions & 25 deletions protocol/x/clob/keeper/msg_server_place_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ func (k Keeper) HandleMsgPlaceOrder(

// 2. Return an error if an associated cancellation or removal already exists in the current block.
processProposerMatchesEvents := k.GetProcessProposerMatchesEvents(ctx)
cancelledOrderIds := lib.UniqueSliceToSet(processProposerMatchesEvents.PlacedStatefulCancellationOrderIds)
if _, found := cancelledOrderIds[order.GetOrderId()]; found {
return errorsmod.Wrapf(
types.ErrStatefulOrderPreviouslyCancelled,
"PlaceOrder: order (%+v)",
order,
)
if !isInternalOrder { // If vault order, we allow the order to replace a cancelled order with the same order ID
cancelledOrderIds := lib.UniqueSliceToSet(processProposerMatchesEvents.PlacedStatefulCancellationOrderIds)
if _, found := cancelledOrderIds[order.GetOrderId()]; found {
return errorsmod.Wrapf(
types.ErrStatefulOrderPreviouslyCancelled,
"PlaceOrder: order (%+v)",
order,
)
}
}
removedOrderIds := lib.UniqueSliceToSet(processProposerMatchesEvents.RemovedStatefulOrderIds)
if _, found := removedOrderIds[order.GetOrderId()]; found {
Expand All @@ -115,31 +117,35 @@ func (k Keeper) HandleMsgPlaceOrder(

// 4. Emit the new order placement indexer event.
if order.IsConditionalOrder() {
k.GetIndexerEventManager().AddTxnEvent(
ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewConditionalOrderPlacementEvent(
order,
if !isInternalOrder {
k.GetIndexerEventManager().AddTxnEvent(
ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewConditionalOrderPlacementEvent(
order,
),
),
),
)
)
}
processProposerMatchesEvents.PlacedConditionalOrderIds = append(
processProposerMatchesEvents.PlacedConditionalOrderIds,
order.OrderId,
)
} else {
k.GetIndexerEventManager().AddTxnEvent(
ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewLongTermOrderPlacementEvent(
order,
if !isInternalOrder {
k.GetIndexerEventManager().AddTxnEvent(
ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewLongTermOrderPlacementEvent(
order,
),
),
),
)
)
}
processProposerMatchesEvents.PlacedLongTermOrderIds = append(
processProposerMatchesEvents.PlacedLongTermOrderIds,
order.OrderId,
Expand Down
3 changes: 1 addition & 2 deletions protocol/x/clob/keeper/msg_server_place_order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,13 +435,12 @@ func TestHandleMsgPlaceOrder(t *testing.T) {
removalExists: false,
equityTierLimitExists: true,
},
"Error - Place an Internal Order, Order Already Cancelled": {
"Success - Place an Internal Order, Order Already Cancelled": {
isInternalOrder: true,
assetQuantums: -1_000_000_000,
cancellationExists: true,
removalExists: false,
equityTierLimitExists: true,
expectedError: types.ErrStatefulOrderPreviouslyCancelled,
},
"Error - Place an Internal Order, Order Already Removed": {
isInternalOrder: true,
Expand Down
1 change: 1 addition & 0 deletions protocol/x/clob/types/clob_keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type ClobKeeper interface {
HandleMsgCancelOrder(
ctx sdk.Context,
msg *MsgCancelOrder,
isInternalOrder bool,
) (err error)
HandleMsgPlaceOrder(
ctx sdk.Context,
Expand Down
Loading
Loading