code.vegaprotocol.io/vega@v0.79.0/core/execution/future/margin_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 future_test 17 18 import ( 19 "context" 20 "testing" 21 "time" 22 23 "code.vegaprotocol.io/vega/core/types" 24 vegacontext "code.vegaprotocol.io/vega/libs/context" 25 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 26 "code.vegaprotocol.io/vega/libs/num" 27 28 "github.com/golang/mock/gomock" 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/require" 31 ) 32 33 func TestMargins(t *testing.T) { 34 party1, party2, party3 := "party1", "party2", "party3" 35 now := time.Unix(10, 0) 36 tm := getTestMarket2(t, now, nil, &types.AuctionDuration{ 37 Duration: 1, 38 // increase lpRange so that LP orders don't get pushed too close to MID and test can behave as expected 39 }, true, 1) 40 price := num.NewUint(100) 41 size := uint64(100) 42 43 addAccount(t, tm, party1) 44 addAccount(t, tm, party2) 45 addAccount(t, tm, party3) 46 tm.broker.EXPECT().Send(gomock.Any()).AnyTimes() 47 48 auxParty := "auxParty" 49 auxParty2 := "auxParty2" 50 addAccount(t, tm, auxParty) 51 addAccount(t, tm, auxParty2) 52 addAccountWithAmount(tm, "lpprov", 100000) 53 54 // set auction durations to 1 second 55 tm.market.OnMarketAuctionMinimumDurationUpdate(context.Background(), time.Second) 56 alwaysOnBid := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "alwaysOnBid", types.SideBuy, auxParty, 1, 1) 57 conf, err := tm.market.SubmitOrder(context.Background(), alwaysOnBid) 58 require.NotNil(t, conf) 59 require.NoError(t, err) 60 require.Equal(t, types.OrderStatusActive, conf.Order.Status) 61 62 alwaysOnAsk := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "alwaysOnAsk", types.SideSell, auxParty, 1, 100000) 63 conf, err = tm.market.SubmitOrder(context.Background(), alwaysOnAsk) 64 require.NotNil(t, conf) 65 require.NoError(t, err) 66 require.Equal(t, types.OrderStatusActive, conf.Order.Status) 67 68 auxOrders := []*types.Order{ 69 getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "aux1", types.SideBuy, auxParty, 1, price.Uint64()), 70 getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "aux2", types.SideSell, auxParty2, 1, price.Uint64()), 71 } 72 for _, o := range auxOrders { 73 conf, err := tm.market.SubmitOrder(context.Background(), o) 74 require.NotNil(t, conf) 75 require.NoError(t, err) 76 } 77 lp := &types.LiquidityProvisionSubmission{ 78 MarketID: tm.market.GetID(), 79 CommitmentAmount: num.NewUint(500), 80 Fee: num.DecimalFromFloat(0.01), 81 } 82 require.NoError(t, tm.market.SubmitLiquidityProvision(context.Background(), lp, "lpprov", vgcrypto.RandomHash())) 83 84 now = now.Add(2 * time.Second) 85 // leave opening auction 86 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 87 tm.now = now 88 tm.market.OnTick(ctx, now) 89 data := tm.market.GetMarketData() 90 require.Equal(t, types.MarketTradingModeContinuous, data.MarketTradingMode) 91 92 order1 := &types.Order{ 93 Status: types.OrderStatusActive, 94 Type: types.OrderTypeLimit, 95 TimeInForce: types.OrderTimeInForceGTC, 96 ID: "someid12", 97 Side: types.SideBuy, 98 Party: party2, 99 MarketID: tm.market.GetID(), 100 Size: size, 101 Price: price.Clone(), 102 Remaining: size, 103 CreatedAt: now.UnixNano(), 104 Reference: "party2-buy-order", 105 } 106 order2 := &types.Order{ 107 Status: types.OrderStatusActive, 108 Type: types.OrderTypeLimit, 109 TimeInForce: types.OrderTimeInForceGTC, 110 ID: "someid123", 111 Side: types.SideSell, 112 Party: party3, 113 MarketID: tm.market.GetID(), 114 Size: size, 115 Price: price.Clone(), 116 Remaining: size, 117 CreatedAt: now.UnixNano(), 118 Reference: "party3-buy-order", 119 } 120 _, err = tm.market.SubmitOrder(context.TODO(), order1) 121 assert.NoError(t, err) 122 confirmation, err := tm.market.SubmitOrder(context.TODO(), order2) 123 assert.NoError(t, err) 124 assert.Equal(t, 1, len(confirmation.Trades)) 125 126 orderBuy := &types.Order{ 127 Status: types.OrderStatusActive, 128 Type: types.OrderTypeLimit, 129 TimeInForce: types.OrderTimeInForceGTC, 130 ID: "someid", 131 Side: types.SideBuy, 132 Party: party1, 133 MarketID: tm.market.GetID(), 134 Size: size, 135 Price: price.Clone(), 136 Remaining: size, 137 CreatedAt: now.UnixNano(), 138 Reference: "party1-buy-order", 139 } 140 // Create an order to amend 141 confirmation, err = tm.market.SubmitOrder(context.TODO(), orderBuy) 142 if !assert.NoError(t, err) { 143 t.Fatalf("Error: %v", err) 144 } 145 if !assert.NotNil(t, confirmation) { 146 t.Fatal("SubmitOrder confirmation was nil, but no error.") 147 } 148 149 orderID := confirmation.Order.ID 150 151 // Amend size up 152 amend := &types.OrderAmendment{ 153 OrderID: orderID, 154 MarketID: tm.market.GetID(), 155 SizeDelta: 10000, 156 } 157 amendment, err := tm.market.AmendOrder(context.TODO(), amend, party1, vgcrypto.RandomHash()) 158 assert.NotNil(t, amendment) 159 assert.NoError(t, err) 160 161 // Amend price and size up to breach margin 162 amend.SizeDelta = 1000000000 163 amend.Price = num.NewUint(1000000000) 164 amendment, err = tm.market.AmendOrder(context.TODO(), amend, party1, vgcrypto.RandomHash()) 165 assert.Nil(t, amendment) 166 assert.Error(t, err) 167 } 168 169 /* Check that a failed new order margin check cannot be got around by amending 170 * an existing order to the same values as the failed new order. */ 171 func TestPartialFillMargins(t *testing.T) { 172 party1 := "party1" 173 party2 := "party2" 174 party3 := "party3" 175 auxParty, auxParty2 := "auxParty", "auxParty2" 176 now := time.Unix(10, 0) 177 tm := getTestMarket(t, now, nil, &types.AuctionDuration{ 178 Duration: 1, 179 }) 180 181 addAccount(t, tm, party1) 182 addAccount(t, tm, party2) 183 addAccount(t, tm, party3) 184 addAccount(t, tm, auxParty) 185 addAccount(t, tm, auxParty2) 186 addAccountWithAmount(tm, "lpprov", 100000000) 187 tm.broker.EXPECT().Send(gomock.Any()).AnyTimes() 188 189 // ensure auction durations are 1 second 190 tm.market.OnMarketAuctionMinimumDurationUpdate(context.Background(), time.Second) 191 alwaysOnBid := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "alwaysOnBid", types.SideBuy, auxParty, 1, 1) 192 conf, err := tm.market.SubmitOrder(context.Background(), alwaysOnBid) 193 require.NotNil(t, conf) 194 require.NoError(t, err) 195 require.Equal(t, types.OrderStatusActive, conf.Order.Status) 196 197 alwaysOnAsk := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "alwaysOnAsk", types.SideSell, auxParty, 1, 1000000000) 198 conf, err = tm.market.SubmitOrder(context.Background(), alwaysOnAsk) 199 require.NotNil(t, conf) 200 require.NoError(t, err) 201 require.Equal(t, types.OrderStatusActive, conf.Order.Status) 202 // create orders so we can leave opening auction 203 auxOrders := []*types.Order{ 204 getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "aux1", types.SideBuy, auxParty, 1, 10000000), 205 getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "aux2", types.SideSell, auxParty2, 1, 10000000), 206 } 207 for _, o := range auxOrders { 208 conf, err := tm.market.SubmitOrder(context.Background(), o) 209 require.NotNil(t, conf) 210 require.NoError(t, err) 211 } 212 lp := &types.LiquidityProvisionSubmission{ 213 MarketID: tm.market.GetID(), 214 CommitmentAmount: num.NewUint(30000000), 215 Fee: num.DecimalFromFloat(0.01), 216 } 217 require.NoError(t, tm.market.SubmitLiquidityProvision(context.Background(), lp, "lpprov", vgcrypto.RandomHash())) 218 now = now.Add(time.Second * 2) // opening auction is 1 second, move time ahead by 2 seconds so we leave auction 219 tm.now = now 220 tm.market.OnTick(vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()), now) 221 222 // use party 2+3 to set super high mark price 223 orderSell1 := &types.Order{ 224 Type: types.OrderTypeLimit, 225 TimeInForce: types.OrderTimeInForceGTC, 226 Side: types.SideSell, 227 Party: party2, 228 MarketID: tm.market.GetID(), 229 Size: 1, 230 Price: num.NewUint(10000000), 231 Remaining: 1, 232 CreatedAt: now.UnixNano(), 233 ExpiresAt: now.UnixNano() + 10000000000, 234 Reference: "party2-sell-order", 235 } 236 confirmation, err := tm.market.SubmitOrder(context.TODO(), orderSell1) 237 require.NoError(t, err) 238 require.NotNil(t, confirmation) 239 240 // other side of the instant match 241 orderBuy1 := &types.Order{ 242 Type: types.OrderTypeMarket, 243 TimeInForce: types.OrderTimeInForceIOC, 244 Side: types.SideBuy, 245 Party: party3, 246 MarketID: tm.market.GetID(), 247 Size: 1, 248 Price: num.UintZero(), 249 Remaining: 1, 250 CreatedAt: now.UnixNano(), 251 Reference: "party3-buy-order", 252 } 253 254 confirmation, err = tm.market.SubmitOrder(context.TODO(), orderBuy1) 255 if !assert.NoError(t, err) { 256 t.Fatalf("Error: %v", err) 257 } 258 if !assert.NotNil(t, confirmation) { 259 t.Fatal("SubmitOrder confirmation was nil, but no error.") 260 } 261 262 // Create a valid smaller order 263 orderBuy3 := &types.Order{ 264 Type: types.OrderTypeLimit, 265 TimeInForce: types.OrderTimeInForceGTT, 266 Side: types.SideBuy, 267 Party: party1, 268 MarketID: tm.market.GetID(), 269 Size: 1, 270 Price: num.NewUint(2), 271 Remaining: 1, 272 CreatedAt: now.UnixNano(), 273 ExpiresAt: now.UnixNano() + 10000000000, 274 Reference: "party1-buy-order", 275 } 276 confirmation, err = tm.market.SubmitOrder(context.TODO(), orderBuy3) 277 if !assert.NoError(t, err) { 278 t.Fatalf("Error: %v", err) 279 } 280 if !assert.NotNil(t, confirmation) { 281 t.Fatal("SubmitOrder confirmation was nil, but no error.") 282 } 283 orderID := confirmation.Order.ID 284 285 // Attempt to amend it to the same size as the failed new order 286 amend := &types.OrderAmendment{ 287 OrderID: orderID, 288 MarketID: tm.market.GetID(), 289 SizeDelta: 999, 290 } 291 amendment, err := tm.market.AmendOrder(context.TODO(), amend, party1, vgcrypto.RandomHash()) 292 assert.Nil(t, amendment) 293 assert.Error(t, err) 294 } 295 296 // TODO karel - fix this tests 297 // func TestMarginRequirementSkippedWhenReducingExposure(t *testing.T) { 298 // ctx := context.Background() 299 // party1 := "party1" 300 // party2 := "party2" 301 // party3 := "party3" 302 // party4 := "party4" 303 // auxParty, auxParty2 := "auxParty", "auxParty2" 304 // now := time.Unix(10, 0) 305 // tm := getTestMarket(t, now, nil, &types.AuctionDuration{ 306 // Duration: 1, 307 // }) 308 309 // addAccount(t, tm, party1) 310 // addAccountWithAmount(tm, party2, 3000) 311 // addAccountWithAmount(tm, party3, 1990) 312 // addAccountWithAmount(tm, party4, 80) 313 // addAccount(t, tm, auxParty) 314 // addAccount(t, tm, auxParty2) 315 // addAccountWithAmount(tm, "lpprov", 100000000) 316 // tm.broker.EXPECT().Send(gomock.Any()).AnyTimes() 317 318 // // Assure liquidity auction won't be triggered 319 // // ensure auction durations are 1 second 320 // tm.market.OnMarketAuctionMinimumDurationUpdate(ctx, time.Second) 321 // alwaysOnBid := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "alwaysOnBid", types.SideBuy, auxParty, 1, 500) 322 // conf, err := tm.market.SubmitOrder(ctx, alwaysOnBid) 323 // require.NotNil(t, conf) 324 // require.NoError(t, err) 325 // require.Equal(t, types.OrderStatusActive, conf.Order.Status) 326 327 // alwaysOnAsk := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "alwaysOnAsk", types.SideSell, auxParty, 1, 1500) 328 // conf, err = tm.market.SubmitOrder(ctx, alwaysOnAsk) 329 // require.NotNil(t, conf) 330 // require.NoError(t, err) 331 // require.Equal(t, types.OrderStatusActive, conf.Order.Status) 332 // // create orders so we can leave opening auction 333 // auxOrders := []*types.Order{ 334 // getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "aux1", types.SideBuy, auxParty, 1, 1000), 335 // getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, "aux2", types.SideSell, auxParty2, 1, 1000), 336 // } 337 // for _, o := range auxOrders { 338 // conf, err := tm.market.SubmitOrder(ctx, o) 339 // require.NotNil(t, conf) 340 // require.NoError(t, err) 341 // } 342 // lp := &types.LiquidityProvisionSubmission{ 343 // MarketID: tm.market.GetID(), 344 // CommitmentAmount: num.NewUint(30000000), 345 // Fee: num.DecimalFromFloat(0.01), 346 // } 347 // require.NoError(t, tm.market.SubmitLiquidityProvision(ctx, lp, "lpprov", vgcrypto.RandomHash())) 348 349 // party4Order := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, party4, types.SideBuy, party4, 1, uint64(500)) 350 // _, err = tm.market.SubmitOrder(ctx, party4Order) 351 // require.ErrorContains(t, err, "margin") 352 353 // now = now.Add(time.Second * 2) // opening auction is 1 second, move time ahead by 2 seconds so we leave auction 354 // tm.now = now 355 // tm.market.OnTick(vegacontext.WithTraceID(ctx, vgcrypto.RandomHash()), now) 356 357 // posSize := uint64(10) 358 // matchingPrice := uint64(1000) 359 // party2Order := getMarketOrder(tm, now, types.OrderTypeLimit, types.OrderTimeInForceGTC, party2, types.SideSell, party2, posSize, matchingPrice) 360 // confirmation, err := tm.market.SubmitOrder(ctx, party2Order) 361 // require.NoError(t, err) 362 // require.NotNil(t, confirmation) 363 364 // party3Order := getMarketOrder(tm, now, types.OrderTypeMarket, types.OrderTimeInForceIOC, party3, types.SideBuy, party3, posSize, matchingPrice) 365 366 // confirmation, err = tm.market.SubmitOrder(ctx, party3Order) 367 // require.NoError(t, err) 368 // require.NotNil(t, confirmation) 369 // require.Equal(t, 1, len(confirmation.Trades)) 370 371 // // both parties low on margin 372 // bal2 := tm.PartyGeneralAccount(t, party2).Balance 373 // bal3 := tm.PartyGeneralAccount(t, party3).Balance 374 // require.True(t, bal2.LT(num.NewUint(50)), bal2.String()) 375 // require.True(t, bal3.LT(num.NewUint(50)), bal3.String()) 376 377 // // parties try to place more limit orders in the same direction and fail 378 // changeSizeTo(party2Order, 1) 379 // party2Order.Type = types.OrderTypeMarket 380 381 // changeSizeTo(party3Order, 1) 382 // party3Order.Type = types.OrderTypeMarket 383 384 // _, err = tm.market.SubmitOrder(ctx, party2Order) 385 // require.ErrorContains(t, err, "margin") 386 // _, err = tm.market.SubmitOrder(ctx, party3Order) 387 // require.ErrorContains(t, err, "margin") 388 389 // // parties try to reduce position with market order of size greater than position and fail 390 // party2Order.Side = types.SideBuy 391 // changeSizeTo(party2Order, posSize+1) 392 // party2Order.Type = types.OrderTypeMarket 393 394 // party3Order.Side = types.SideSell 395 // changeSizeTo(party3Order, posSize+1) 396 // party3Order.Type = types.OrderTypeMarket 397 398 // _, err = tm.market.SubmitOrder(ctx, party2Order) 399 // require.ErrorContains(t, err, "margin") 400 // _, err = tm.market.SubmitOrder(ctx, party3Order) 401 // require.ErrorContains(t, err, "margin") 402 403 // // parties try to reduce position with passive limit order of size less than position and succeed 404 // changeSizeTo(party2Order, posSize-2) 405 // party2Order.Type = types.OrderTypeLimit 406 // party2Order.TimeInForce = types.OrderTimeInForceGTC 407 // party2Order.Price = num.UintZero().Sub(num.NewUint(matchingPrice), num.NewUint(501)) 408 409 // changeSizeTo(party3Order, posSize-2) 410 // party3Order.Type = types.OrderTypeLimit 411 // party3Order.TimeInForce = types.OrderTimeInForceGTC 412 // party3Order.Price = num.UintZero().Add(num.NewUint(matchingPrice), num.NewUint(501)) 413 414 // conf, err = tm.market.SubmitOrder(ctx, party2Order) 415 // require.NoError(t, err) 416 // require.Empty(t, conf.Trades) 417 // conf, err = tm.market.SubmitOrder(ctx, party3Order) 418 // require.NoError(t, err) 419 // require.Empty(t, conf.Trades) 420 421 // // parties try to place a pegged order in the same opposite direction and fail 422 // party2Order.PeggedOrder = &types.PeggedOrder{Reference: types.PeggedReferenceMid, Offset: num.NewUint(10)} 423 // changeSizeTo(party2Order, 1) 424 // _, err = tm.market.SubmitOrder(ctx, party2Order) 425 // require.ErrorContains(t, err, "margin") 426 // party2Order.PeggedOrder = nil 427 428 // party3Order.PeggedOrder = &types.PeggedOrder{Reference: types.PeggedReferenceMid, Offset: num.NewUint(10)} 429 // changeSizeTo(party2Order, 1) 430 // _, err = tm.market.SubmitOrder(ctx, party3Order) 431 // require.ErrorContains(t, err, "margin") 432 // party3Order.PeggedOrder = nil 433 434 // // parties place more passive limit orders so that the total order size is greater than position size and fail 435 // changeSizeTo(party2Order, 5) 436 // changeSizeTo(party3Order, 5) 437 438 // _, err = tm.market.SubmitOrder(ctx, party2Order) 439 // require.ErrorContains(t, err, "margin") 440 // _, err = tm.market.SubmitOrder(ctx, party3Order) 441 // require.ErrorContains(t, err, "margin") 442 443 // // parties successfully reduce their position with market order 444 // partialReduction := uint64(5) 445 // changeSizeTo(party2Order, partialReduction) 446 // party2Order.Type = types.OrderTypeMarket 447 // party2Order.TimeInForce = types.OrderTimeInForceFOK 448 449 // changeSizeTo(party3Order, partialReduction) 450 // party3Order.Type = types.OrderTypeMarket 451 // party3Order.TimeInForce = types.OrderTimeInForceFOK 452 453 // _, err = tm.market.SubmitOrder(ctx, party2Order) 454 // require.NoError(t, err) 455 // _, err = tm.market.SubmitOrder(ctx, party3Order) 456 // require.NoError(t, err) 457 458 // // parties try to close position with order with size equal to initial position size but fail as the position is now smaller 459 // changeSizeTo(party2Order, posSize-partialReduction) 460 // changeSizeTo(party3Order, posSize-partialReduction) 461 462 // _, err = tm.market.SubmitOrder(ctx, party3Order) 463 // require.NoError(t, err) 464 // _, err = tm.market.SubmitOrder(ctx, party2Order) 465 // require.NoError(t, err) 466 // } 467 468 // func changeSizeTo(ord *types.Order, size uint64) { 469 // ord.Size = size 470 // ord.Remaining = size 471 // }