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 }