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

[CT-1327] place post only orders first in prepare check state #2618

Merged
merged 6 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
10 changes: 5 additions & 5 deletions protocol/mocks/MemClob.go

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

26 changes: 26 additions & 0 deletions protocol/testutil/constants/stateful_orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,19 @@ var (
}

// Long-Term post-only orders.
LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5_PO = clobtypes.Order{
OrderId: clobtypes.OrderId{
SubaccountId: Alice_Num0,
ClientId: 0,
OrderFlags: clobtypes.OrderIdFlags_LongTerm,
ClobPairId: 0,
},
Side: clobtypes.Order_SIDE_BUY,
Quantums: 5,
Subticks: 10,
GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 5},
TimeInForce: clobtypes.Order_TIME_IN_FORCE_POST_ONLY,
}
LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15_PO = clobtypes.Order{
OrderId: clobtypes.OrderId{
SubaccountId: Alice_Num0,
Expand Down Expand Up @@ -1360,6 +1373,19 @@ var (
GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 10},
TimeInForce: clobtypes.Order_TIME_IN_FORCE_POST_ONLY,
}
LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10_PO = clobtypes.Order{
OrderId: clobtypes.OrderId{
SubaccountId: Dave_Num0,
ClientId: 0,
OrderFlags: clobtypes.OrderIdFlags_LongTerm,
ClobPairId: 0,
},
Side: clobtypes.Order_SIDE_SELL,
Quantums: 25_000_000,
Subticks: 50_000_000_000,
GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 10},
TimeInForce: clobtypes.Order_TIME_IN_FORCE_POST_ONLY,
}

// Long-Term reduce-only orders.
LongTermOrder_Bob_Num0_Id2_Clob0_Sell10_Price35_GTB20_RO = clobtypes.Order{
Expand Down
47 changes: 37 additions & 10 deletions protocol/x/clob/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,42 @@ func PrepareCheckState(
offchainUpdates,
)

// 3. Place all stateful order placements included in the last block on the memclob.
// 3. Go through the orders two times and only place the post only orders during the first pass.
longTermOrderIds := keeper.GetDeliveredLongTermOrderIds(ctx)
offchainUpdates = keeper.PlaceStatefulOrdersFromLastBlock(
ctx,
longTermOrderIds,
offchainUpdates,
true, // post only
)

offchainUpdates = keeper.PlaceConditionalOrdersTriggeredInLastBlock(
ctx,
processProposerMatchesEvents.ConditionalOrderIdsTriggeredInLastBlock,
offchainUpdates,
true, // post only
)

replayUpdates := keeper.MemClob.ReplayOperations(
ctx,
localValidatorOperationsQueue,
shortTermOrderTxBytes,
offchainUpdates,
true, // post only
)
if replayUpdates != nil {
offchainUpdates = replayUpdates
}

// 4. Place all stateful order placements included in the last block on the memclob.
// Note telemetry is measured outside of the function call because `PlaceStatefulOrdersFromLastBlock`
// is called within `PlaceConditionalOrdersTriggeredInLastBlock`.
startPlaceLongTermOrders := time.Now()
longTermOrderIds := keeper.GetDeliveredLongTermOrderIds(ctx)
offchainUpdates = keeper.PlaceStatefulOrdersFromLastBlock(
ctx,
longTermOrderIds,
offchainUpdates,
false, // post only
)
telemetry.MeasureSince(
startPlaceLongTermOrders,
Expand All @@ -208,27 +235,27 @@ func PrepareCheckState(
metrics.Count,
)

// 4. Place all conditional orders triggered in EndBlocker of last block on the memclob.
// 5. Place all conditional orders triggered in EndBlocker of last block on the memclob.
offchainUpdates = keeper.PlaceConditionalOrdersTriggeredInLastBlock(
ctx,
processProposerMatchesEvents.ConditionalOrderIdsTriggeredInLastBlock,
offchainUpdates,
false, // post only
)

// 5. Replay the local validator’s operations onto the book.
replayUpdates := keeper.MemClob.ReplayOperations(
// 6. Replay the local validator’s operations onto the book.
replayUpdates = keeper.MemClob.ReplayOperations(
ctx,
localValidatorOperationsQueue,
shortTermOrderTxBytes,
offchainUpdates,
false, // post only
)

// TODO(CLOB-275): Do not gracefully handle panics in `PrepareCheckState`.
if replayUpdates != nil {
offchainUpdates = replayUpdates
}

// 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them.
// 7. Get all potentially liquidatable subaccount IDs and attempt to liquidate them.
liquidatableSubaccountIds := keeper.DaemonLiquidationInfo.GetLiquidatableSubaccountIds()
subaccountsToDeleverage, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, liquidatableSubaccountIds)
if err != nil {
Expand All @@ -241,14 +268,14 @@ func PrepareCheckState(
keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets(ctx)...,
)

// 7. Deleverage subaccounts.
// 8. Deleverage subaccounts.
// TODO(CLOB-1052) - decouple steps 6 and 7 by using DaemonLiquidationInfo.NegativeTncSubaccounts
// as the input for this function.
if err := keeper.DeleverageSubaccounts(ctx, subaccountsToDeleverage); err != nil {
panic(err)
}

// 8. Gate withdrawals by inserting a zero-fill deleveraging operation into the operations queue if any
// 9. Gate withdrawals by inserting a zero-fill deleveraging operation into the operations queue if any
// of the negative TNC subaccounts still have negative TNC after liquidations and deleveraging steps.
negativeTncSubaccountIds := keeper.DaemonLiquidationInfo.GetNegativeTncSubaccountIds()
if err := keeper.GateWithdrawalsIfNegativeTncSubaccountSeen(ctx, negativeTncSubaccountIds); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions protocol/x/clob/e2e/conditional_orders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ func TestConditionalOrder(t *testing.T) {
constants.Dave_Num0_500000USD,
},
orders: []clobtypes.Order{
constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10,
constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10_PO,
constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO,
},
priceUpdateForFirstBlock: &prices.MsgUpdateMarketPrices{
Expand Down Expand Up @@ -1869,7 +1869,7 @@ func TestConditionalOrder_TriggeringUsingMatchedPrice(t *testing.T) {
constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10,
constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10,
// Place the conditional order.
constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10,
constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10_PO,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential issue with using a post-only order in this test case

In the test case "TakeProfit/Buy post-only conditional order can place, trigger, cross, and be removed from state", you have changed the order to a post-only order by using LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10_PO. Post-only orders are intended to add liquidity and will not execute immediately against existing orders. This may prevent the expected cross and removal from state, which contradicts the test's objective to verify that the conditional order can cross and be removed from state.

Consider using a non-post-only order to ensure the test behaves as intended.

Apply this diff to fix the issue:

-				constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10_PO,
+				constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10,
📝 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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10_PO,
constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10,

constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO,
},
expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{
Expand Down
4 changes: 2 additions & 2 deletions protocol/x/clob/e2e/order_removal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestConditionalOrderRemoval(t *testing.T) {
constants.Bob_Num0_10_000USD,
},
orders: []clobtypes.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5,
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5_PO,
constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell10_Price10_GTBT10_PO_SL_15,
},

Expand Down Expand Up @@ -629,7 +629,7 @@ func TestOrderRemoval(t *testing.T) {
constants.Alice_Num0_10_000USD,
constants.Bob_Num0_10_000USD,
},
firstOrder: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5,
firstOrder: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5_PO,
secondOrder: constants.LongTermOrder_Bob_Num0_Id0_Clob0_Sell10_Price10_GTBT10_PO,

expectedFirstOrderRemoved: false,
Expand Down
15 changes: 14 additions & 1 deletion protocol/x/clob/keeper/orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ func (k Keeper) PlaceStatefulOrdersFromLastBlock(
ctx sdk.Context,
placedStatefulOrderIds []types.OrderId,
existingOffchainUpdates *types.OffchainUpdates,
onlyPlacePostOnly bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: current naming might suggest that if false, the function processes ALL orders including post-only. How abt sth like postOnlyFilter and comment explicitly what this var does?

) (
offchainUpdates *types.OffchainUpdates,
) {
Expand All @@ -521,6 +522,12 @@ func (k Keeper) PlaceStatefulOrdersFromLastBlock(
}

order := orderPlacement.GetOrder()

// Skip the order if it is a post-only order and we are only placing post-only orders.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: comment could be more accurate/complete - right now it just describes one of the two scenarios

if onlyPlacePostOnly != order.IsPostOnlyOrder() {
continue
}

// Validate and place order.
_, orderStatus, placeOrderOffchainUpdates, err := k.AddPreexistingStatefulOrder(
ctx,
Expand Down Expand Up @@ -579,6 +586,7 @@ func (k Keeper) PlaceConditionalOrdersTriggeredInLastBlock(
ctx sdk.Context,
conditionalOrderIdsTriggeredInLastBlock []types.OrderId,
existingOffchainUpdates *types.OffchainUpdates,
onlyPlacePostOnly bool,
) (
offchainUpdates *types.OffchainUpdates,
) {
Expand Down Expand Up @@ -608,7 +616,12 @@ func (k Keeper) PlaceConditionalOrdersTriggeredInLastBlock(
}
}

return k.PlaceStatefulOrdersFromLastBlock(ctx, conditionalOrderIdsTriggeredInLastBlock, existingOffchainUpdates)
return k.PlaceStatefulOrdersFromLastBlock(
ctx,
conditionalOrderIdsTriggeredInLastBlock,
existingOffchainUpdates,
onlyPlacePostOnly,
)
}

// PerformOrderCancellationStatefulValidation performs stateful validation on an order cancellation.
Expand Down
140 changes: 137 additions & 3 deletions protocol/x/clob/keeper/orders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2218,7 +2218,139 @@ func TestPlaceStatefulOrdersFromLastBlock(t *testing.T) {
for _, order := range tc.orders {
orderIds = append(orderIds, order.OrderId)
}
ks.ClobKeeper.PlaceStatefulOrdersFromLastBlock(ctx, orderIds, offchainUpdates)
ks.ClobKeeper.PlaceStatefulOrdersFromLastBlock(ctx, orderIds, offchainUpdates, true)
ks.ClobKeeper.PlaceStatefulOrdersFromLastBlock(ctx, orderIds, offchainUpdates, false)

// PlaceStatefulOrdersFromLastBlock utilizes the memclob's PlaceOrder flow, but we
// do not want to emit PlaceMessages in offchain events for stateful orders. This assertion
// verifies that we call `ClearPlaceMessages()` on the offchain updates before returning.
require.Equal(t, 0, memclobtest.MessageCountOfType(offchainUpdates, types.PlaceMessageType))

// Verify that all removed orders have an associated off-chain update.
orderMap := make(map[types.OrderId]bool)
for _, order := range tc.orders {
orderMap[order.OrderId] = true
}

removedOrders := lib.FilterSlice(tc.expectedOrderPlacementCalls, func(order types.Order) bool {
return !orderMap[order.OrderId]
})

for _, order := range removedOrders {
require.True(
t,
memclobtest.HasMessage(offchainUpdates, order.OrderId, types.RemoveMessageType),
)
}

memClob.AssertExpectations(t)
})
}
}

func TestPlaceStatefulOrdersFromLastBlock_PostOnly(t *testing.T) {
tests := map[string]struct {
orders []types.Order
onlyPlacePostOnly bool

expectedOrderPlacementCalls []types.Order
}{
"places PO stateful orders from last block when onlyPlacePostOnly = true": {
onlyPlacePostOnly: true,
orders: []types.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5_PO,
},
expectedOrderPlacementCalls: []types.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5_PO,
},
},
"does not places non-PO stateful orders when onlyPlacePostOnly = true": {
onlyPlacePostOnly: true,
orders: []types.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5,
},
expectedOrderPlacementCalls: []types.Order{},
},
"does not places PO stateful orders from last block, when onlyPlacePostOnly = false": {
onlyPlacePostOnly: false,
orders: []types.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5_PO,
},
expectedOrderPlacementCalls: []types.Order{},
},
"places non-PO stateful orders from last block when onlyPlacePostOnly = false": {
onlyPlacePostOnly: false,
orders: []types.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5,
},
expectedOrderPlacementCalls: []types.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT5,
},
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// Setup state.
memClob := &mocks.MemClob{}

memClob.On("SetClobKeeper", mock.Anything).Return()

ks := keepertest.NewClobKeepersTestContext(
t,
memClob,
&mocks.BankKeeper{},
indexer_manager.NewIndexerEventManagerNoop(),
)

ks.MarketMapKeeper.InitGenesis(ks.Ctx, constants.MarketMap_DefaultGenesisState)
prices.InitGenesis(ks.Ctx, *ks.PricesKeeper, constants.Prices_DefaultGenesisState)
perpetuals.InitGenesis(ks.Ctx, *ks.PerpetualsKeeper, constants.Perpetuals_DefaultGenesisState)

ctx := ks.Ctx.WithBlockHeight(int64(100)).WithBlockTime(time.Unix(5, 0))
ctx = ctx.WithIsCheckTx(true)
ks.BlockTimeKeeper.SetPreviousBlockInfo(ctx, &blocktimetypes.BlockInfo{
Height: 100,
Timestamp: time.Unix(int64(2), 0),
})

// Create CLOB pair.
memClob.On("CreateOrderbook", constants.ClobPair_Btc).Return()
_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
ctx,
constants.ClobPair_Btc.Id,
clobtest.MustPerpetualId(constants.ClobPair_Btc),
satypes.BaseQuantums(constants.ClobPair_Btc.StepBaseQuantums),
constants.ClobPair_Btc.QuantumConversionExponent,
constants.ClobPair_Btc.SubticksPerTick,
constants.ClobPair_Btc.Status,
)
require.NoError(t, err)

// Create each stateful order placement in state
for i, order := range tc.orders {
require.True(t, order.IsStatefulOrder())

ks.ClobKeeper.SetLongTermOrderPlacement(ctx.WithIsCheckTx(false), order, uint32(i))
}

// Assert expected order placement memclob calls.
for _, order := range tc.expectedOrderPlacementCalls {
memClob.On("PlaceOrder", mock.Anything, order).Return(
satypes.BaseQuantums(0),
types.Success,
constants.TestOffchainUpdates,
nil,
).Once()
}

// Run the test and verify expectations.
offchainUpdates := types.NewOffchainUpdates()
orderIds := make([]types.OrderId, 0)
for _, order := range tc.orders {
orderIds = append(orderIds, order.OrderId)
}
ks.ClobKeeper.PlaceStatefulOrdersFromLastBlock(ctx, orderIds, offchainUpdates, tc.onlyPlacePostOnly)

// PlaceStatefulOrdersFromLastBlock utilizes the memclob's PlaceOrder flow, but we
// do not want to emit PlaceMessages in offchain events for stateful orders. This assertion
Expand Down Expand Up @@ -2376,13 +2508,15 @@ func TestPlaceConditionalOrdersTriggeredInLastBlock(t *testing.T) {
t,
tc.expectedPanic,
func() {
ks.ClobKeeper.PlaceConditionalOrdersTriggeredInLastBlock(ctx, orderIds, offchainUpdates)
ks.ClobKeeper.PlaceConditionalOrdersTriggeredInLastBlock(ctx, orderIds, offchainUpdates, true)
ks.ClobKeeper.PlaceConditionalOrdersTriggeredInLastBlock(ctx, orderIds, offchainUpdates, false)
},
)
return
}

ks.ClobKeeper.PlaceConditionalOrdersTriggeredInLastBlock(ctx, orderIds, offchainUpdates)
ks.ClobKeeper.PlaceConditionalOrdersTriggeredInLastBlock(ctx, orderIds, offchainUpdates, true)
ks.ClobKeeper.PlaceConditionalOrdersTriggeredInLastBlock(ctx, orderIds, offchainUpdates, false)

// PlaceStatefulOrdersFromLastBlock utilizes the memclob's PlaceOrder flow, but we
// do not want to emit PlaceMessages in offchain events for stateful orders. This assertion
Expand Down
Loading
Loading