code.vegaprotocol.io/vega@v0.79.0/core/execution/stoporders/stop_orders_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package stoporders_test
    17  
    18  import (
    19  	"testing"
    20  	"time"
    21  
    22  	"code.vegaprotocol.io/vega/core/execution/stoporders"
    23  	"code.vegaprotocol.io/vega/core/types"
    24  	"code.vegaprotocol.io/vega/libs/num"
    25  	"code.vegaprotocol.io/vega/logging"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  )
    29  
    30  func TestSingleStopOrders(t *testing.T) {
    31  	pool := stoporders.New(logging.NewTestLogger())
    32  
    33  	pool.PriceUpdated(num.NewUint(50))
    34  
    35  	// this is going to trigger when going up 60
    36  	pool.Insert(newPricedStopOrder("a", "p1", "", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
    37  	pool.Insert(newPricedStopOrder("b", "p1", "", num.NewUint(57), types.StopOrderTriggerDirectionRisesAbove))
    38  
    39  	// this will be triggered when going from 60 to 57, and trigger the falls below
    40  	pool.Insert(newTrailingStopOrder("c", "p2", "", num.MustDecimalFromString("0.05"), types.StopOrderTriggerDirectionFallsBelow))
    41  	pool.Insert(newTrailingStopOrder("d", "p2", "", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
    42  
    43  	// mixing around both, will be triggered by the end
    44  	pool.Insert(newPricedStopOrder("e", "p2", "", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
    45  	pool.Insert(newTrailingStopOrder("f", "p2", "", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
    46  
    47  	// mixing around both, will be triggered by the end
    48  	pool.Insert(newPricedStopOrderWithOverride("g", "p2", "", num.MustDecimalFromString("1.0"), num.NewUint(20), types.StopOrderTriggerDirectionFallsBelow))
    49  	pool.Insert(newTrailingStopOrderWithOverride("h", "p2", "", num.MustDecimalFromString("1.0"), num.MustDecimalFromString("1"), types.StopOrderTriggerDirectionRisesAbove))
    50  
    51  	assert.Equal(t, pool.Len(), 8)
    52  
    53  	// move the price a little, nothing should happen.
    54  	triggeredOrders, cancelledOrders := pool.PriceUpdated(num.NewUint(55))
    55  	assert.Len(t, triggeredOrders, 0)
    56  	assert.Len(t, cancelledOrders, 0)
    57  
    58  	t.Run("price move triggers priced stop order", func(t *testing.T) {
    59  		// move the price so the priced stop order is triggered, this should return both
    60  		triggeredOrders, cancelledOrders = pool.PriceUpdated(num.NewUint(60))
    61  		assert.Len(t, triggeredOrders, 1)
    62  		assert.Len(t, cancelledOrders, 0)
    63  		assert.Equal(t, pool.Len(), 7)
    64  		assert.Equal(t, triggeredOrders[0].Status, types.StopOrderStatusTriggered)
    65  		assert.Equal(t, triggeredOrders[0].ID, "b")
    66  	})
    67  
    68  	t.Run("trying to remove a triggered order returns an error", func(t *testing.T) {
    69  		// try to remove it now. no errors, the party have no orders anymore
    70  		affectedOrders, err := pool.Cancel("p1", "b")
    71  		assert.Len(t, affectedOrders, 0)
    72  		assert.EqualError(t, err, "stop order not found")
    73  	})
    74  
    75  	t.Run("price update trigger trailing order", func(t *testing.T) {
    76  		// move the price so the trailing order get triggered
    77  		triggeredOrders, cancelledOrders = pool.PriceUpdated(num.NewUint(57))
    78  		assert.Len(t, triggeredOrders, 1)
    79  		assert.Len(t, cancelledOrders, 0)
    80  		assert.Equal(t, pool.Len(), 6)
    81  		assert.Equal(t, triggeredOrders[0].Status, types.StopOrderStatusTriggered)
    82  		assert.Equal(t, triggeredOrders[0].ID, "c")
    83  	})
    84  
    85  	t.Run("trying to remove a triggered order returns an error", func(t *testing.T) {
    86  		// try to remove it now. no errors, the party have no orders anymore
    87  		affectedOrders, err := pool.Cancel("p2", "c")
    88  		assert.Len(t, affectedOrders, 0)
    89  		assert.EqualError(t, err, "stop order not found")
    90  	})
    91  
    92  	t.Run("price update trigger trailing order/priced order", func(t *testing.T) {
    93  		// move the price so the trailing order get triggered
    94  		triggeredOrders, cancelledOrders = pool.PriceUpdated(num.NewUint(75))
    95  		assert.Len(t, triggeredOrders, 2)
    96  		assert.Len(t, cancelledOrders, 0)
    97  		assert.Equal(t, pool.Len(), 4)
    98  		assert.Equal(t, triggeredOrders[0].Status, types.StopOrderStatusTriggered)
    99  		assert.Equal(t, triggeredOrders[0].ID, "d")
   100  		assert.Equal(t, triggeredOrders[1].Status, types.StopOrderStatusTriggered)
   101  		assert.Equal(t, triggeredOrders[1].ID, "f")
   102  	})
   103  }
   104  
   105  func TestCancelStopOrders(t *testing.T) {
   106  	pool := stoporders.New(logging.NewTestLogger())
   107  
   108  	pool.PriceUpdated(num.NewUint(50))
   109  
   110  	pool.Insert(newPricedStopOrder("a", "p1", "", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   111  	pool.Insert(newPricedStopOrder("b", "p1", "", num.NewUint(57), types.StopOrderTriggerDirectionRisesAbove))
   112  
   113  	pool.Insert(newTrailingStopOrder("c", "p2", "", num.MustDecimalFromString("0.05"), types.StopOrderTriggerDirectionFallsBelow))
   114  	pool.Insert(newTrailingStopOrder("d", "p2", "", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   115  
   116  	pool.Insert(newPricedStopOrder("e", "p2", "f", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   117  	pool.Insert(newTrailingStopOrder("f", "p2", "e", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   118  
   119  	pool.Insert(newPricedStopOrder("h", "p2", "i", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   120  	pool.Insert(newTrailingStopOrder("i", "p2", "h", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   121  
   122  	// a party with no order returns no error
   123  	affectedOrders, err := pool.Cancel("p3", "")
   124  	assert.NoError(t, err)
   125  	assert.Len(t, affectedOrders, 0)
   126  
   127  	// remove one order, not OCO
   128  	affectedOrders, err = pool.Cancel("p1", "b")
   129  	assert.NoError(t, err)
   130  	assert.Len(t, affectedOrders, 1)
   131  	assert.Equal(t, affectedOrders[0].ID, "b")
   132  	assert.Equal(t, affectedOrders[0].Status, types.StopOrderStatusCancelled)
   133  	assert.Equal(t, 7, pool.Len())
   134  
   135  	// remove one order, OCO, to returned
   136  	affectedOrders, err = pool.Cancel("p2", "f")
   137  	assert.NoError(t, err)
   138  	assert.Len(t, affectedOrders, 2)
   139  	assert.Equal(t, affectedOrders[0].ID, "f")
   140  	assert.Equal(t, affectedOrders[0].Status, types.StopOrderStatusCancelled)
   141  	assert.Equal(t, affectedOrders[1].ID, "e")
   142  	assert.Equal(t, affectedOrders[1].Status, types.StopOrderStatusCancelled)
   143  	assert.Equal(t, 5, pool.Len())
   144  
   145  	// remove all for party
   146  	affectedOrders, err = pool.Cancel("p2", "")
   147  	assert.NoError(t, err)
   148  	assert.Len(t, affectedOrders, 4)
   149  	assert.Equal(t, affectedOrders[0].ID, "c")
   150  	assert.Equal(t, affectedOrders[0].Status, types.StopOrderStatusCancelled)
   151  	assert.Equal(t, affectedOrders[1].ID, "d")
   152  	assert.Equal(t, affectedOrders[1].Status, types.StopOrderStatusCancelled)
   153  	assert.Equal(t, affectedOrders[2].ID, "h")
   154  	assert.Equal(t, affectedOrders[2].Status, types.StopOrderStatusCancelled)
   155  	assert.Equal(t, affectedOrders[3].ID, "i")
   156  	assert.Equal(t, affectedOrders[3].Status, types.StopOrderStatusCancelled)
   157  	assert.Equal(t, 1, pool.Len())
   158  
   159  	// ensure the actual trees are cleaned up
   160  	assert.Equal(t, 0, pool.Trailing().Len(types.StopOrderTriggerDirectionFallsBelow))
   161  	assert.Equal(t, 0, pool.Trailing().Len(types.StopOrderTriggerDirectionRisesAbove))
   162  	assert.Equal(t, 1, pool.Priced().Len(types.StopOrderTriggerDirectionFallsBelow))
   163  	assert.Equal(t, 0, pool.Priced().Len(types.StopOrderTriggerDirectionRisesAbove))
   164  }
   165  
   166  func TestRemoveExpiredStopOrders(t *testing.T) {
   167  	pool := stoporders.New(logging.NewTestLogger())
   168  
   169  	pool.PriceUpdated(num.NewUint(50))
   170  
   171  	pool.Insert(newPricedStopOrder("a", "p1", "", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   172  	pool.Insert(newPricedStopOrder("b", "p1", "", num.NewUint(57), types.StopOrderTriggerDirectionRisesAbove))
   173  
   174  	pool.Insert(newTrailingStopOrder("c", "p2", "", num.MustDecimalFromString("0.05"), types.StopOrderTriggerDirectionFallsBelow))
   175  	pool.Insert(newTrailingStopOrder("d", "p2", "", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   176  
   177  	pool.Insert(newPricedStopOrder("e", "p2", "f", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   178  	pool.Insert(newTrailingStopOrder("f", "p2", "e", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   179  
   180  	pool.Insert(newPricedStopOrder("h", "p2", "i", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   181  	pool.Insert(newTrailingStopOrder("i", "p2", "h", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   182  
   183  	assert.Equal(t, 1, pool.Trailing().Len(types.StopOrderTriggerDirectionFallsBelow))
   184  	assert.Equal(t, 1, pool.Trailing().Len(types.StopOrderTriggerDirectionRisesAbove))
   185  	assert.Equal(t, 1, pool.Priced().Len(types.StopOrderTriggerDirectionFallsBelow))
   186  	assert.Equal(t, 1, pool.Priced().Len(types.StopOrderTriggerDirectionRisesAbove))
   187  
   188  	// expire b and f, should return 3 orders
   189  	affectedOrders := pool.RemoveExpired([]string{"b", "f"})
   190  	assert.Len(t, affectedOrders, 3)
   191  	assert.Equal(t, affectedOrders[0].ID, "b")
   192  	assert.Equal(t, affectedOrders[0].Status, types.StopOrderStatusExpired)
   193  	assert.Equal(t, affectedOrders[1].ID, "e")
   194  	assert.Equal(t, affectedOrders[1].Status, types.StopOrderStatusStopped)
   195  	assert.Equal(t, affectedOrders[2].ID, "f")
   196  	assert.Equal(t, affectedOrders[2].Status, types.StopOrderStatusExpired)
   197  
   198  	// ensure the actual trees are cleaned up
   199  	assert.Equal(t, 1, pool.Trailing().Len(types.StopOrderTriggerDirectionFallsBelow))
   200  	assert.Equal(t, 1, pool.Trailing().Len(types.StopOrderTriggerDirectionRisesAbove))
   201  	assert.Equal(t, 1, pool.Priced().Len(types.StopOrderTriggerDirectionFallsBelow))
   202  	assert.Equal(t, 0, pool.Priced().Len(types.StopOrderTriggerDirectionRisesAbove))
   203  }
   204  
   205  func TestCannotSubmitSameOrderTwice(t *testing.T) {
   206  	pool := stoporders.New(logging.NewTestLogger())
   207  
   208  	pool.PriceUpdated(num.NewUint(50))
   209  	pool.Insert(newPricedStopOrder("a", "p1", "b", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   210  	assert.Panics(t, func() {
   211  		pool.Insert(newPricedStopOrder("a", "p1", "b", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   212  	})
   213  }
   214  
   215  func TestOCOStopOrders(t *testing.T) {
   216  	pool := stoporders.New(logging.NewTestLogger())
   217  
   218  	pool.PriceUpdated(num.NewUint(50))
   219  
   220  	// this is going to trigger when going up 60, and cancel a
   221  	pool.Insert(newPricedStopOrder("a", "p1", "b", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   222  	pool.Insert(newPricedStopOrder("b", "p1", "a", num.NewUint(57), types.StopOrderTriggerDirectionRisesAbove))
   223  
   224  	// this will be triggered when going from 60 to 57, and triggre the falls below + cancel d
   225  	pool.Insert(newTrailingStopOrder("c", "p2", "d", num.MustDecimalFromString("0.05"), types.StopOrderTriggerDirectionFallsBelow))
   226  	pool.Insert(newTrailingStopOrder("d", "p2", "c", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   227  
   228  	// mixing around both, will be triggered by the end
   229  	pool.Insert(newPricedStopOrder("e", "p2", "f", num.NewUint(40), types.StopOrderTriggerDirectionFallsBelow))
   230  	pool.Insert(newTrailingStopOrder("f", "p2", "e", num.MustDecimalFromString("0.5"), types.StopOrderTriggerDirectionRisesAbove))
   231  
   232  	assert.Equal(t, pool.Len(), 6)
   233  
   234  	// move the price a little, nothing should happen.
   235  	triggeredOrders, cancelledOrders := pool.PriceUpdated(num.NewUint(55))
   236  	assert.Len(t, triggeredOrders, 0)
   237  	assert.Len(t, cancelledOrders, 0)
   238  
   239  	t.Run("price move triggers priced stop order", func(t *testing.T) {
   240  		// move the price so the priced stop order is triggered, this should return both
   241  		triggeredOrders, cancelledOrders = pool.PriceUpdated(num.NewUint(60))
   242  		assert.Len(t, triggeredOrders, 1)
   243  		assert.Len(t, cancelledOrders, 1)
   244  		assert.Equal(t, pool.Len(), 4)
   245  		assert.Equal(t, triggeredOrders[0].Status, types.StopOrderStatusTriggered)
   246  		assert.Equal(t, cancelledOrders[0].Status, types.StopOrderStatusStopped)
   247  		assert.Equal(t, triggeredOrders[0].ID, "b")
   248  		assert.Equal(t, cancelledOrders[0].ID, "a")
   249  	})
   250  
   251  	// try to remove it now. no errors, the party have no orders anymore
   252  	t.Run("removing when party have submitted nothing returns no error", func(t *testing.T) {
   253  		affectedOrders, err := pool.Cancel("p1", "a")
   254  		assert.Len(t, affectedOrders, 0)
   255  		assert.EqualError(t, err, stoporders.ErrStopOrderNotFound.Error())
   256  	})
   257  
   258  	t.Run("price update trigger OCO trailing order", func(t *testing.T) {
   259  		// move the price so the trailing order get triggered
   260  		triggeredOrders, cancelledOrders = pool.PriceUpdated(num.NewUint(57))
   261  		assert.Len(t, triggeredOrders, 1)
   262  		assert.Len(t, cancelledOrders, 1)
   263  		assert.Equal(t, pool.Len(), 2)
   264  		assert.Equal(t, triggeredOrders[0].Status, types.StopOrderStatusTriggered)
   265  		assert.Equal(t, cancelledOrders[0].Status, types.StopOrderStatusStopped)
   266  		assert.Equal(t, triggeredOrders[0].ID, "c")
   267  		assert.Equal(t, cancelledOrders[0].ID, "d")
   268  	})
   269  
   270  	t.Run("trying to remove a triggered order returns an error", func(t *testing.T) {
   271  		// try to remove it now. no errors, the party have no orders anymore
   272  		affectedOrders, err := pool.Cancel("p2", "c")
   273  		assert.Len(t, affectedOrders, 0)
   274  		assert.EqualError(t, err, "stop order not found")
   275  	})
   276  
   277  	t.Run("price update trigger OCO trailing order/priced order", func(t *testing.T) {
   278  		// move the price so the trailing order get triggered
   279  		triggeredOrders, cancelledOrders = pool.PriceUpdated(num.NewUint(75))
   280  		assert.Len(t, triggeredOrders, 1)
   281  		assert.Len(t, cancelledOrders, 1)
   282  		assert.Equal(t, pool.Len(), 0)
   283  		assert.Equal(t, triggeredOrders[0].Status, types.StopOrderStatusTriggered)
   284  		assert.Equal(t, cancelledOrders[0].Status, types.StopOrderStatusStopped)
   285  		assert.Equal(t, triggeredOrders[0].ID, "f")
   286  		assert.Equal(t, cancelledOrders[0].ID, "e")
   287  	})
   288  }
   289  
   290  func newPricedStopOrder(
   291  	id, party, ocoLinkID string,
   292  	price *num.Uint,
   293  	direction types.StopOrderTriggerDirection,
   294  ) *types.StopOrder {
   295  	return &types.StopOrder{
   296  		ID:        id,
   297  		Party:     party,
   298  		OCOLinkID: ocoLinkID,
   299  		Trigger:   types.NewPriceStopOrderTrigger(direction, price),
   300  		Expiry:    &types.StopOrderExpiry{}, // no expiry, not important here
   301  		CreatedAt: time.Now(),
   302  		UpdatedAt: time.Now().Add(10 * time.Second),
   303  		Status:    types.StopOrderStatusPending,
   304  		OrderSubmission: &types.OrderSubmission{
   305  			MarketID:    "some",
   306  			Type:        types.OrderTypeMarket,
   307  			ReduceOnly:  true,
   308  			Size:        10,
   309  			TimeInForce: types.OrderTimeInForceIOC,
   310  			Side:        types.SideBuy,
   311  		},
   312  	}
   313  }
   314  
   315  func newPricedStopOrderWithOverride(
   316  	id, party, ocoLinkID string,
   317  	sizeOverrideScale num.Decimal,
   318  	price *num.Uint,
   319  	direction types.StopOrderTriggerDirection,
   320  ) *types.StopOrder {
   321  	return &types.StopOrder{
   322  		ID:        id,
   323  		Party:     party,
   324  		OCOLinkID: ocoLinkID,
   325  		Trigger:   types.NewPriceStopOrderTrigger(direction, price),
   326  		Expiry:    &types.StopOrderExpiry{}, // no expiry, not important here
   327  		CreatedAt: time.Now(),
   328  		UpdatedAt: time.Now().Add(10 * time.Second),
   329  		Status:    types.StopOrderStatusPending,
   330  		OrderSubmission: &types.OrderSubmission{
   331  			MarketID:    "some",
   332  			Type:        types.OrderTypeMarket,
   333  			ReduceOnly:  true,
   334  			Size:        10,
   335  			TimeInForce: types.OrderTimeInForceIOC,
   336  			Side:        types.SideBuy,
   337  		},
   338  		SizeOverrideSetting: types.StopOrderSizeOverrideSettingPosition,
   339  		SizeOverrideValue:   &types.StopOrderSizeOverrideValue{PercentageSize: sizeOverrideScale},
   340  	}
   341  }
   342  
   343  //nolint:unparam
   344  func newTrailingStopOrder(
   345  	id, party, ocoLinkID string,
   346  	offset num.Decimal,
   347  	direction types.StopOrderTriggerDirection,
   348  ) *types.StopOrder {
   349  	return &types.StopOrder{
   350  		ID:        id,
   351  		Party:     party,
   352  		OCOLinkID: ocoLinkID,
   353  		Trigger:   types.NewTrailingStopOrderTrigger(direction, offset),
   354  		Expiry:    &types.StopOrderExpiry{}, // no expiry, not important here
   355  		CreatedAt: time.Now(),
   356  		UpdatedAt: time.Now().Add(10 * time.Second),
   357  		Status:    types.StopOrderStatusPending,
   358  		OrderSubmission: &types.OrderSubmission{
   359  			MarketID:    "some",
   360  			Type:        types.OrderTypeMarket,
   361  			ReduceOnly:  true,
   362  			Size:        10,
   363  			TimeInForce: types.OrderTimeInForceIOC,
   364  			Side:        types.SideBuy,
   365  		},
   366  	}
   367  }
   368  
   369  //nolint:unparam
   370  func newTrailingStopOrderWithOverride(
   371  	id, party, ocoLinkID string,
   372  	sizeOverrideScale num.Decimal,
   373  	offset num.Decimal,
   374  	direction types.StopOrderTriggerDirection,
   375  ) *types.StopOrder {
   376  	return &types.StopOrder{
   377  		ID:        id,
   378  		Party:     party,
   379  		OCOLinkID: ocoLinkID,
   380  		Trigger:   types.NewTrailingStopOrderTrigger(direction, offset),
   381  		Expiry:    &types.StopOrderExpiry{}, // no expiry, not important here
   382  		CreatedAt: time.Now(),
   383  		UpdatedAt: time.Now().Add(10 * time.Second),
   384  		Status:    types.StopOrderStatusPending,
   385  		OrderSubmission: &types.OrderSubmission{
   386  			MarketID:    "some",
   387  			Type:        types.OrderTypeMarket,
   388  			ReduceOnly:  true,
   389  			Size:        10,
   390  			TimeInForce: types.OrderTimeInForceIOC,
   391  			Side:        types.SideBuy,
   392  		},
   393  		SizeOverrideSetting: types.StopOrderSizeOverrideSettingPosition,
   394  		SizeOverrideValue:   &types.StopOrderSizeOverrideValue{PercentageSize: sizeOverrideScale},
   395  	}
   396  }