From 09ec10955d403b04a8f672a06323b58735f88762 Mon Sep 17 00:00:00 2001 From: jayy04 <103467857+jayy04@users.noreply.github.com> Date: Wed, 10 Jan 2024 19:00:21 -0500 Subject: [PATCH] =?UTF-8?q?[CLOB-1054]=20trigger=20conditional=20orders=20?= =?UTF-8?q?using=20traded=20price=20within=20the=20=E2=80=A6=20(#945)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [CLOB-1054] trigger conditional orders using traded price within the block * comments * fix lint --- protocol/app/app.go | 1 + protocol/testutil/constants/orders.go | 28 + .../testutil/constants/stateful_orders.go | 56 + protocol/testutil/constants/subaccounts.go | 14 + protocol/testutil/keeper/clob.go | 3 + .../x/clob/e2e/conditional_orders_test.go | 1139 +++++++++++++++++ protocol/x/clob/keeper/keeper.go | 3 + .../x/clob/keeper/process_single_match.go | 3 + .../keeper/untriggered_conditional_orders.go | 48 + protocol/x/clob/types/constants.go | 24 + protocol/x/clob/types/expected_keepers.go | 4 + protocol/x/clob/types/memclob.go | 9 - 12 files changed, 1323 insertions(+), 9 deletions(-) create mode 100644 protocol/x/clob/types/constants.go diff --git a/protocol/app/app.go b/protocol/app/app.go index c965bf41d6..24b1e6469a 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -932,6 +932,7 @@ func New( app.BankKeeper, app.FeeTiersKeeper, app.PerpetualsKeeper, + app.PricesKeeper, app.StatsKeeper, app.RewardsKeeper, app.IndexerEventManager, diff --git a/protocol/testutil/constants/orders.go b/protocol/testutil/constants/orders.go index 01ab55c0b8..52e04a030a 100644 --- a/protocol/testutil/constants/orders.go +++ b/protocol/testutil/constants/orders.go @@ -813,6 +813,20 @@ var ( Subticks: 50_000_000_000, GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, } + Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Carl_Num1, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, + Subticks: 50_003_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } + Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50500_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Carl_Num1, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, + Subticks: 50_500_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } Order_Dave_Num0_Id2_Clob0_Sell1BTC_Price49500_GTB10 = clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: Dave_Num0, ClientId: 2, ClobPairId: 0}, Side: clobtypes.Order_SIDE_SELL, @@ -961,6 +975,20 @@ var ( Subticks: 3_030_000_000, GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, } + Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49500_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Dave_Num1, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 49_500_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } + Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Dave_Num1, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 49_997_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } Order_Dave_Num1_Id0_Clob0_Sell025BTC_Price49999_GTB10 = clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: Dave_Num1, ClientId: 0, ClobPairId: 0}, Side: clobtypes.Order_SIDE_SELL, diff --git a/protocol/testutil/constants/stateful_orders.go b/protocol/testutil/constants/stateful_orders.go index a7060580a6..8b32ad8b30 100644 --- a/protocol/testutil/constants/stateful_orders.go +++ b/protocol/testutil/constants/stateful_orders.go @@ -862,6 +862,20 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_TAKE_PROFIT, ConditionalOrderTriggerSubticks: 20, } + ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49700 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Alice_Num0, + ClientId: 0, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, + Subticks: 50_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 10}, + ConditionType: clobtypes.Order_CONDITION_TYPE_TAKE_PROFIT, + ConditionalOrderTriggerSubticks: 49_700_000_000, + } ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Alice_Num0, @@ -933,6 +947,20 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, ConditionalOrderTriggerSubticks: 50_005_000_000, } + ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50300 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Alice_Num0, + ClientId: 0, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, + Subticks: 50_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 10}, + ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, + ConditionalOrderTriggerSubticks: 50_300_000_000, + } ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Bob_Num0, @@ -961,6 +989,34 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_TAKE_PROFIT, ConditionalOrderTriggerSubticks: 50_005_000_000, } + ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50300 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Bob_Num0, + ClientId: 0, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 50_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 10}, + ConditionType: clobtypes.Order_CONDITION_TYPE_TAKE_PROFIT, + ConditionalOrderTriggerSubticks: 50_300_000_000, + } + ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49700 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Bob_Num0, + ClientId: 0, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 50_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 10}, + ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, + ConditionalOrderTriggerSubticks: 49_700_000_000, + } ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Bob_Num0, diff --git a/protocol/testutil/constants/subaccounts.go b/protocol/testutil/constants/subaccounts.go index e8e5bd5230..790d9f4910 100644 --- a/protocol/testutil/constants/subaccounts.go +++ b/protocol/testutil/constants/subaccounts.go @@ -283,6 +283,13 @@ var ( }, PerpetualPositions: []*satypes.PerpetualPosition{}, } + Carl_Num1_100000USD = satypes.Subaccount{ + Id: &Carl_Num1, + AssetPositions: []*satypes.AssetPosition{ + &Usdc_Asset_100_000, + }, + PerpetualPositions: []*satypes.PerpetualPosition{}, + } Carl_Num1_Short_500USD = satypes.Subaccount{ Id: &Carl_Num1, AssetPositions: []*satypes.AssetPosition{ @@ -529,6 +536,13 @@ var ( }, PerpetualPositions: []*satypes.PerpetualPosition{}, } + Dave_Num1_500000USD = satypes.Subaccount{ + Id: &Dave_Num1, + AssetPositions: []*satypes.AssetPosition{ + &Usdc_Asset_500_000, + }, + PerpetualPositions: []*satypes.PerpetualPosition{}, + } Dave_Num1_025BTC_Long_50000USD = satypes.Subaccount{ Id: &Dave_Num1, AssetPositions: []*satypes.AssetPosition{ diff --git a/protocol/testutil/keeper/clob.go b/protocol/testutil/keeper/clob.go index c3fa6513ea..1c69eba08f 100644 --- a/protocol/testutil/keeper/clob.go +++ b/protocol/testutil/keeper/clob.go @@ -141,6 +141,7 @@ func NewClobKeepersTestContextWithUninitializedMemStore( bankKeeper, ks.FeeTiersKeeper, ks.PerpetualsKeeper, + ks.PricesKeeper, ks.StatsKeeper, ks.RewardsKeeper, ks.SubaccountsKeeper, @@ -177,6 +178,7 @@ func createClobKeeper( bankKeeper types.BankKeeper, feeTiersKeeper types.FeeTiersKeeper, perpKeeper *perpkeeper.Keeper, + pricesKeeper *priceskeeper.Keeper, statsKeeper *statskeeper.Keeper, rewardsKeeper types.RewardsKeeper, saKeeper *subkeeper.Keeper, @@ -207,6 +209,7 @@ func createClobKeeper( bankKeeper, feeTiersKeeper, perpKeeper, + pricesKeeper, statsKeeper, rewardsKeeper, indexerEventManager, diff --git a/protocol/x/clob/e2e/conditional_orders_test.go b/protocol/x/clob/e2e/conditional_orders_test.go index d9763c09b7..ce1d5f7160 100644 --- a/protocol/x/clob/e2e/conditional_orders_test.go +++ b/protocol/x/clob/e2e/conditional_orders_test.go @@ -10,6 +10,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + "github.com/dydxprotocol/v4-chain/protocol/testutil/daemons/pricefeed/exchange_config" "github.com/dydxprotocol/v4-chain/protocol/testutil/encoding" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" feetiertypes "github.com/dydxprotocol/v4-chain/protocol/x/feetiers/types" @@ -958,6 +959,1144 @@ func TestConditionalOrder(t *testing.T) { } } +func TestConditionalOrder_TriggeringUsingMatchedPrice(t *testing.T) { + tests := map[string]struct { + subaccounts []satypes.Subaccount + ordersForFirstBlock []clobtypes.Order + ordersForSecondBlock []clobtypes.Order + + expectedInTriggeredStateAfterBlock map[uint32]map[clobtypes.OrderId]bool + + // these expectations are asserted after all blocks are processed + expectedExistInState map[clobtypes.OrderId]bool + expectedOrderFillAmount map[clobtypes.OrderId]uint64 + expectedSubaccounts []satypes.Subaccount + }{ + "TakeProfit/Buy conditional order is placed and not triggered by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: false}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: false}, + }, + }, + "TakeProfit/Buy conditional order is placed and not triggered by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Trigger price is $49,700. + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49700, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,500. + // This price can trigger the conditional order if unbounded. + // The bounded price is $50,000 - $50,000 * 0.5% = $49,750, which would not trigger the conditional order. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49500_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: false}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49995.OrderId: false}, + }, + }, + "StopLoss/Buy conditional order is placed and not triggered by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: false}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: false}, + }, + }, + "StopLoss/Buy conditional order is placed and not triggered by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Trigger price is $50,300. + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50300, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,500. + // This price can trigger the conditional order if unbounded. + // The bounded price is $50,000 + $50,000 * 0.5% = $50,250, which would not trigger the conditional order. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50500_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: false}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50005.OrderId: false}, + }, + }, + "TakeProfit/Sell conditional order is placed and not triggered by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: false}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: false}, + }, + }, + "TakeProfit/Sell conditional order is placed and not triggered by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Trigger price is $50,300. + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50300, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,500. + // This price can trigger the conditional order if unbounded. + // The bounded price is $50,000 + $50,000 * 0.5% = $50,250, which would not trigger the conditional order. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50500_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: false}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005.OrderId: false}, + }, + }, + "StopLoss/Sell conditional order is placed and not triggered by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: false}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: false}, + }, + }, + "StopLoss/Sell conditional order is placed and not triggered by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Trigger price is $49,700. + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49700, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,500. + // This price can trigger the conditional order if unbounded. + // The bounded price is $50,000 - $50,000 * 0.5% = $49,750, which would not trigger the conditional order. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49500_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: false}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49995.OrderId: false}, + }, + }, + "TakeProfit/Buy conditional order is placed and triggered immediately by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + }, + }, + "TakeProfit/Buy conditional order is placed and triggered immediately by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $49,500. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49500_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + }, + }, + "TakeProfit/Buy conditional order is placed and triggered in later blocks": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + }, + }, + "TakeProfit/Buy conditional order is placed and triggered in later blocks (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,500. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49500_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + }, + }, + "StopLoss/Buy conditional order is placed and triggered immediately by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + }, + }, + "StopLoss/Buy conditional order is placed and triggered immediately by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,500. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50500_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + }, + }, + "StopLoss/Buy conditional order is placed and triggered in later blocks": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + }, + }, + "StopLoss/Buy conditional order is placed and triggered in later blocks (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,500. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50500_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + }, + }, + "TakeProfit/Sell conditional order is placed and triggered immediately by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + }, + }, + "TakeProfit/Sell conditional order is placed and triggered immediately by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,500. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50500_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + }, + }, + "TakeProfit/Sell conditional order is placed and triggered in later blocks": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + }, + }, + "TakeProfit/Sell conditional order is placed and triggered in later blocks (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,500. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50500_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + }, + }, + "StopLoss/Sell conditional order is placed and triggered immediately by matched price": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + }, + }, + "StopLoss/Sell conditional order is placed and triggered immediately by matched price (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $49,500. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49500_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + }, + }, + "StopLoss/Sell conditional order is placed and triggered in later blocks": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + }, + }, + "StopLoss/Sell conditional order is placed and triggered in later blocks (bounded)": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,500. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49500_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + }, + }, + "TakeProfit/Buy conditional order is placed, triggered, and partially matched": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Dave_Num0_500000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + // Place the order that would match against the conditional order. + constants.Order_Dave_Num0_Id1_Clob0_Sell025BTC_Price50000_GTB11, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: true}, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.Order_Dave_Num0_Id1_Clob0_Sell025BTC_Price50000_GTB11.OrderId: 25_000_000, + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_TP_49999.OrderId: 25_000_000, + }, + }, + "StopLoss/Buy conditional order is placed, triggered, and partially matched": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + constants.Dave_Num0_500000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the order that would match against the conditional order. + constants.Order_Dave_Num0_Id1_Clob0_Sell025BTC_Price50000_GTB11, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: false}, + 3: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: true}, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.Order_Dave_Num0_Id1_Clob0_Sell025BTC_Price50000_GTB11.OrderId: 25_000_000, + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001.OrderId: 25_000_000, + }, + }, + "TakeProfit/Sell conditional order is placed, triggered, and partially matched": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num0_100000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the order that would match against the conditional order. + constants.Order_Carl_Num0_Id3_Clob0_Buy025BTC_Price50000, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: true}, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.Order_Carl_Num0_Id3_Clob0_Buy025BTC_Price50000.OrderId: 25_000_000, + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50001.OrderId: 25_000_000, + }, + }, + "StopLoss/Sell conditional order is placed, triggered, and partially matched": { + subaccounts: []satypes.Subaccount{ + constants.Bob_Num0_100_000USD, + constants.Carl_Num0_100000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999, + }, + ordersForSecondBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + // Place the order that would match against the conditional order. + constants.Order_Carl_Num0_Id3_Clob0_Buy025BTC_Price50000, + }, + + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: false}, + 3: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + 4: {constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: true}, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.Order_Carl_Num0_Id3_Clob0_Buy025BTC_Price50000.OrderId: 25_000_000, + constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_49999.OrderId: 25_000_000, + }, + }, + "StopLoss/Buy IOC conditional order can place, trigger, partially match, and be removed from state": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Dave_Num0_10000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order and the order that would match against it. + constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10, + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: true}, + 3: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: false}, + 4: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: false}, + }, + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: false, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(10_000_000_000 - 12_500_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(25_000_000), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + "TakeProfit/Sell FOK conditional order can place, trigger, not match, and be removed from state": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order. + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: true}, + 3: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: false}, + 4: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: false}, + }, + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: false, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + &constants.Usdc_Asset_10_000, + }, + }, + }, + }, + "TakeProfit/Sell FOK conditional order can place, trigger, fully match, and be removed from state": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Dave_Num0_500000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order and the order that would match against it. + constants.LongTermOrder_Dave_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_PO, + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: true}, + 3: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: false}, + 4: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: false}, + }, + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Sell05BTC_Price50000_GTBT10_TP_50003_FOK.OrderId: false, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(10_000_000_000 + 25_000_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-50_000_000), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + "StopLoss/Buy IOC conditional order can place, trigger, fully match, and be removed from state": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Dave_Num0_500000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $50,003. + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + // Place the conditional order and the order that would match against it. + constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10, + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: true}, + 3: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: false}, + 4: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: false}, + }, + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_SL_50003_IOC.OrderId: false, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(10_000_000_000 - 25_000_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(50_000_000), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + "TakeProfit/Buy post-only conditional order can place, trigger, not cross, and stay in state": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Dave_Num0_500000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $49,997. + constants.Order_Dave_Num1_Id0_Clob0_Sell1BTC_Price49997_GTB10, + constants.Order_Carl_Num1_Id0_Clob0_Buy1BTC_Price50003_GTB10, + // Place the conditional order and the order that would match against it. + constants.LongTermOrder_Dave_Num0_Id1_Clob0_Sell025BTC_Price50001_GTBT10, + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true}, + 3: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true}, + 4: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true}, + }, + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + &constants.Usdc_Asset_10_000, + }, + }, + }, + }, + "TakeProfit/Buy post-only conditional order can place, trigger, not cross, and partially fill in a later block": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Dave_Num0_500000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $49,997. + 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_Id1_Clob0_Sell025BTC_Price50001_GTBT10, + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO, + }, + ordersForSecondBlock: []clobtypes.Order{ + constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell025BTC_Price50000_GTBT10, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true}, + 3: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true}, + 4: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true}, + }, + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: 25_000_000, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(10_000_000_000 - 12_500_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(25_000_000), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + "TakeProfit/Buy post-only conditional order can place, trigger, cross, and be removed from state": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Dave_Num0_500000USD, + constants.Carl_Num1_100000USD, + constants.Dave_Num1_500000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + // Create a match with price $49,997. + 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.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO, + }, + expectedInTriggeredStateAfterBlock: map[uint32]map[clobtypes.OrderId]bool{ + 2: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: true}, + 3: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: false}, + 4: {constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: false}, + }, + expectedExistInState: map[clobtypes.OrderId]bool{ + constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy05BTC_Price50000_GTBT10_TP_49999_PO.OrderId: false, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + &constants.Usdc_Asset_10_000, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *satypes.GenesisState) { + genesisState.Subaccounts = tc.subaccounts + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *prices.GenesisState) { + *genesisState = prices.GenesisState{ + MarketParams: []prices.MarketParam{ + { + Id: 0, + Pair: constants.BtcUsdPair, + Exponent: constants.BtcUsdExponent, + MinExchanges: 1, + MinPriceChangePpm: 1_000, + ExchangeConfigJson: constants.TestMarketExchangeConfigs[exchange_config.MARKET_BTC_USD], + }, + }, + + MarketPrices: []prices.MarketPrice{ + { + Id: 0, + Exponent: constants.BtcUsdExponent, + Price: constants.FiveBillion, // $50,000 == 1 BTC + }, + }, + } + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *perptypes.GenesisState) { + genesisState.Params = constants.PerpetualsGenesisParams + genesisState.LiquidityTiers = constants.LiquidityTiers + genesisState.Perpetuals = []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + } + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *clobtypes.GenesisState) { + genesisState.ClobPairs = []clobtypes.ClobPair{ + { + Id: 0, + Metadata: &clobtypes.ClobPair_PerpetualClobMetadata{ + PerpetualClobMetadata: &clobtypes.PerpetualClobMetadata{ + PerpetualId: 0, + }, + }, + StepBaseQuantums: 1, + SubticksPerTick: 1, + QuantumConversionExponent: -8, + Status: clobtypes.ClobPair_STATUS_ACTIVE, + }, + } + genesisState.LiquidationsConfig = clobtypes.LiquidationsConfig_Default + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *feetiertypes.GenesisState) { + genesisState.Params = constants.PerpetualFeeParamsNoFee + }, + ) + return genesis + }).Build() + ctx := tApp.InitChain() + + // Create all orders. + for _, order := range tc.ordersForFirstBlock { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg( + ctx, + tApp.App, + *clobtypes.NewMsgPlaceOrder(order), + ) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } + } + + ctx = tApp.AdvanceToBlock(2, testapp.AdvanceToBlockOptions{}) + + // First block should persist stateful orders to state. + for _, order := range tc.ordersForFirstBlock { + if order.IsStatefulOrder() { + _, found := tApp.App.ClobKeeper.GetLongTermOrderPlacement(ctx, order.OrderId) + require.True(t, found) + } + } + + if expectedTriggeredOrders, ok := tc.expectedInTriggeredStateAfterBlock[2]; ok { + for orderId, triggered := range expectedTriggeredOrders { + require.Equal(t, triggered, tApp.App.ClobKeeper.IsConditionalOrderTriggered(ctx, orderId), "Block %d", 2) + } + } + + // Place orders for second block + for _, order := range tc.ordersForSecondBlock { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg( + ctx, + tApp.App, + *clobtypes.NewMsgPlaceOrder(order), + ) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } + } + + ctx = tApp.AdvanceToBlock(3, testapp.AdvanceToBlockOptions{}) + + if expectedTriggeredOrders, ok := tc.expectedInTriggeredStateAfterBlock[3]; ok { + for orderId, triggered := range expectedTriggeredOrders { + require.Equal(t, triggered, tApp.App.ClobKeeper.IsConditionalOrderTriggered(ctx, orderId), "Block %d", 3) + } + } + + // Advance to the next block so that matches are proposed and persisted. + ctx = tApp.AdvanceToBlock(4, testapp.AdvanceToBlockOptions{}) + if expectedTriggeredOrders, ok := tc.expectedInTriggeredStateAfterBlock[4]; ok { + for orderId, triggered := range expectedTriggeredOrders { + require.Equal(t, triggered, tApp.App.ClobKeeper.IsConditionalOrderTriggered(ctx, orderId), "Block %d", 4) + } + } + + // Verify expectations. + for orderId, exists := range tc.expectedExistInState { + _, found := tApp.App.ClobKeeper.GetLongTermOrderPlacement(ctx, orderId) + require.Equal(t, exists, found) + } + + for orderId, expectedFillAmount := range tc.expectedOrderFillAmount { + exists, fillAmount, _ := tApp.App.ClobKeeper.GetOrderFillAmount(ctx, orderId) + require.True(t, exists) + require.Equal(t, expectedFillAmount, fillAmount.ToUint64()) + } + + for _, subaccount := range tc.expectedSubaccounts { + actualSubaccount := tApp.App.SubaccountsKeeper.GetSubaccount(ctx, *subaccount.Id) + require.Equal(t, subaccount, actualSubaccount) + } + }) + } +} + func TestConditionalOrderCancellation(t *testing.T) { tests := map[string]struct { subaccounts []satypes.Subaccount diff --git a/protocol/x/clob/keeper/keeper.go b/protocol/x/clob/keeper/keeper.go index 609d49062b..e79827f1f1 100644 --- a/protocol/x/clob/keeper/keeper.go +++ b/protocol/x/clob/keeper/keeper.go @@ -37,6 +37,7 @@ type ( blockTimeKeeper types.BlockTimeKeeper feeTiersKeeper types.FeeTiersKeeper perpetualsKeeper types.PerpetualsKeeper + pricesKeeper types.PricesKeeper statsKeeper types.StatsKeeper rewardsKeeper types.RewardsKeeper indexerEventManager indexer_manager.IndexerEventManager @@ -77,6 +78,7 @@ func NewKeeper( bankKeeper types.BankKeeper, feeTiersKeeper types.FeeTiersKeeper, perpetualsKeeper types.PerpetualsKeeper, + pricesKeeper types.PricesKeeper, statsKeeper types.StatsKeeper, rewardsKeeper types.RewardsKeeper, indexerEventManager indexer_manager.IndexerEventManager, @@ -101,6 +103,7 @@ func NewKeeper( bankKeeper: bankKeeper, feeTiersKeeper: feeTiersKeeper, perpetualsKeeper: perpetualsKeeper, + pricesKeeper: pricesKeeper, statsKeeper: statsKeeper, rewardsKeeper: rewardsKeeper, indexerEventManager: indexerEventManager, diff --git a/protocol/x/clob/keeper/process_single_match.go b/protocol/x/clob/keeper/process_single_match.go index e1e12b7948..6180caae6e 100644 --- a/protocol/x/clob/keeper/process_single_match.go +++ b/protocol/x/clob/keeper/process_single_match.go @@ -454,6 +454,9 @@ func (k Keeper) persistMatchedOrders( ) } + // Update the last trade price for the perpetual. + k.SetTradePricesForPerpetual(ctx, perpetualId, matchWithOrders.MakerOrder.GetOrderSubticks()) + // Process fill in x/stats and x/rewards. k.rewardsKeeper.AddRewardSharesForFill( ctx, diff --git a/protocol/x/clob/keeper/untriggered_conditional_orders.go b/protocol/x/clob/keeper/untriggered_conditional_orders.go index 5afe8a9023..80ffdbbf16 100644 --- a/protocol/x/clob/keeper/untriggered_conditional_orders.go +++ b/protocol/x/clob/keeper/untriggered_conditional_orders.go @@ -286,11 +286,59 @@ func (k Keeper) MaybeTriggerConditionalOrders(ctx sdk.Context) (triggeredConditi ), ) } + + // Trigger conditional orders using the oracle price. currentOraclePriceSubticksRat := k.GetOraclePriceSubticksRat(ctx, clobPair) triggeredOrderIds := untriggeredConditionalOrders.PollTriggeredConditionalOrders( currentOraclePriceSubticksRat, ) triggeredConditionalOrderIds = append(triggeredConditionalOrderIds, triggeredOrderIds...) + + // Trigger conditional orders using the last traded price. + perpetualId := clobPair.MustGetPerpetualId() + minTradePriceSubticks, maxTradePriceSubticks, found := k.GetTradePricesForPerpetual(ctx, perpetualId) + if found { + // Get the perpetual. + perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, perpetualId) + if err != nil { + panic( + fmt.Errorf( + "EndBlocker: untriggeredConditionalOrders failed to find perpetualId %+v", + perpetualId, + ), + ) + } + + // Get the market param. + marketParam, exists := k.pricesKeeper.GetMarketParam(ctx, perpetual.Params.MarketId) + if !exists { + panic( + fmt.Errorf( + "EndBlocker: untriggeredConditionalOrders failed to find marketParam %+v", + perpetual.Params.MarketId, + ), + ) + } + + // Calculate the max allowed range. + maxAllowedRange := lib.BigRatMulPpm(currentOraclePriceSubticksRat, marketParam.MinPriceChangePpm) + maxAllowedRange.Mul(maxAllowedRange, new(big.Rat).SetUint64(types.ConditionalOrderTriggerMultiplier)) + + upperBound := new(big.Rat).Add(currentOraclePriceSubticksRat, maxAllowedRange) + lowerBound := new(big.Rat).Sub(currentOraclePriceSubticksRat, maxAllowedRange) + + for _, price := range []types.Subticks{minTradePriceSubticks, maxTradePriceSubticks} { + // Clamp the min and max trade prices to the upper and lower bounds. + clampedTradePrice := lib.BigRatClamp( + new(big.Rat).SetUint64(price.ToUint64()), + lowerBound, + upperBound, + ) + triggeredOrderIds := untriggeredConditionalOrders.PollTriggeredConditionalOrders(clampedTradePrice) + triggeredConditionalOrderIds = append(triggeredConditionalOrderIds, triggeredOrderIds...) + } + } + // Set the modified untriggeredConditionalOrders back on the keeper field. k.UntriggeredConditionalOrders[clobPairId] = untriggeredConditionalOrders } diff --git a/protocol/x/clob/types/constants.go b/protocol/x/clob/types/constants.go new file mode 100644 index 0000000000..c0eaf75392 --- /dev/null +++ b/protocol/x/clob/types/constants.go @@ -0,0 +1,24 @@ +package types + +import ( + "time" +) + +// ShortBlockWindow represents the maximum number of blocks past the current block height that a +// `MsgPlaceOrder` or `MsgCancelOrder` message will be considered valid by the validator. +const ShortBlockWindow uint32 = 20 + +// StatefulOrderTimeWindow represents the maximum amount of time in seconds past the current block time that a +// long-term/conditional `MsgPlaceOrder` message will be considered valid by the validator. +const StatefulOrderTimeWindow time.Duration = 95 * 24 * time.Hour // 95 days. + +// ConditionalOrderTriggerMultiplier represents the multiplier used to calculate the upper and lower bounds of +// the trigger price for a conditional order. +// The upper bound is calculated as: +// +// upper_bound = (1 + min_price_change_ppm / 1_000_000 * conditional_order_trigger_multiplier) * oracle_price +// +// The lower bound is calculated as: +// +// lower_bound = (1 - min_price_change_ppm / 1_000_000 * conditional_order_trigger_multiplier) * oracle_price +const ConditionalOrderTriggerMultiplier uint64 = 5 diff --git a/protocol/x/clob/types/expected_keepers.go b/protocol/x/clob/types/expected_keepers.go index 97557081f5..d59acece75 100644 --- a/protocol/x/clob/types/expected_keepers.go +++ b/protocol/x/clob/types/expected_keepers.go @@ -141,6 +141,10 @@ type PerpetualsKeeper interface { MaybeProcessNewFundingTickEpoch(ctx sdk.Context) } +type PricesKeeper interface { + GetMarketParam(ctx sdk.Context, id uint32) (param pricestypes.MarketParam, exists bool) +} + type StatsKeeper interface { RecordFill(ctx sdk.Context, takerAddress string, makerAddress string, notional *big.Int) } diff --git a/protocol/x/clob/types/memclob.go b/protocol/x/clob/types/memclob.go index 51c1a63719..d80d5b1345 100644 --- a/protocol/x/clob/types/memclob.go +++ b/protocol/x/clob/types/memclob.go @@ -2,21 +2,12 @@ package types import ( "math/big" - "time" sdk "github.com/cosmos/cosmos-sdk/types" perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) -// ShortBlockWindow represents the maximum number of blocks past the current block height that a -// `MsgPlaceOrder` or `MsgCancelOrder` message will be considered valid by the validator. -const ShortBlockWindow uint32 = 20 - -// StatefulOrderTimeWindow represents the maximum amount of time in seconds past the current block time that a -// long-term/conditional `MsgPlaceOrder` message will be considered valid by the validator. -const StatefulOrderTimeWindow time.Duration = 95 * 24 * time.Hour // 95 days. - // MemClob is an interface that encapsulates all reads and writes to the // CLOB's in-memory data structures. type MemClob interface {