code.vegaprotocol.io/vega@v0.79.0/core/products/perpetual_auctions_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 products_test 17 18 import ( 19 "context" 20 "testing" 21 "time" 22 23 "code.vegaprotocol.io/vega/core/types" 24 "code.vegaprotocol.io/vega/libs/num" 25 26 "github.com/golang/mock/gomock" 27 "github.com/stretchr/testify/assert" 28 "github.com/stretchr/testify/require" 29 ) 30 31 func TestPerpetualsWithAuctions(t *testing.T) { 32 t.Run("funding period is all an auction", testFundingPeriodIsAllAnAuction) 33 t.Run("data point in auction is ignored", testDataPointInAuctionIgnored) 34 t.Run("data points in auction received out of order", testDataPointsInAuctionOutOfOrder) 35 t.Run("auction preserved when period resets", testAuctionFundingPeriodReset) 36 t.Run("funding data in auction period start", testFundingDataAtInAuctionPeriodStart) 37 t.Run("past funding payment calculation", testPastFundingPayment) 38 t.Run("past funding payment calculation in auction", testPastFundingPaymentInAuction) 39 } 40 41 func testFundingPeriodIsAllAnAuction(t *testing.T) { 42 perp := testPerpetual(t) 43 defer perp.ctrl.Finish() 44 45 // set of the data points such that difference in averages is 0 46 points := getTestDataPoints(t) 47 48 // tell the perpetual that we are ready to accept settlement stuff 49 whenLeaveOpeningAuction(t, perp, points[0].t) 50 51 // enter auction 52 whenAuctionStateChanges(t, perp, points[0].t, true) 53 54 // send in some data points with a TWAP difference 55 submitDataWithDifference(t, perp, points, 10) 56 57 // leave auction 58 whenAuctionStateChanges(t, perp, points[len(points)-1].t, false) 59 60 fundingPayment := whenTheFundingPeriodEnds(t, perp, points[len(points)-1].t) 61 assert.Equal(t, "0", fundingPayment.String()) 62 } 63 64 func testDataPointInAuctionIgnored(t *testing.T) { 65 perp := testPerpetual(t) 66 defer perp.ctrl.Finish() 67 expectedTWAP := 100 68 // set of the data points such that difference in averages is 0 69 points := getTestDataPoints(t) 70 require.GreaterOrEqual(t, 4, len(points)) 71 72 // tell the perpetual that we are ready to accept settlement stuff 73 whenLeaveOpeningAuction(t, perp, points[0].t) 74 75 // submit the first point then enter an auction 76 submitPointWithDifference(t, perp, points[0], expectedTWAP) 77 auctionStart := points[0].t + int64(time.Second) 78 whenAuctionStateChanges(t, perp, auctionStart, true) 79 80 // submit a crazy point difference, then a normal point 81 submitPointWithDifference(t, perp, points[1], -9999999) 82 submitPointWithDifference(t, perp, points[2], expectedTWAP) 83 84 // now we leave auction and the crazy point difference will not affect the TWAP because it was in an auction period 85 auctionEnd := points[2].t + int64(time.Second) 86 whenAuctionStateChanges(t, perp, auctionEnd, false) 87 88 currentPeriodLength := float64(points[len(points)-1].t - points[0].t) 89 timeInAuction := float64(auctionEnd - auctionStart) 90 periodFractionOutsideAuction := 1 - timeInAuction/currentPeriodLength 91 92 fundingPayment := whenTheFundingPeriodEnds(t, perp, points[len(points)-1].t) 93 assert.Equal(t, int64(periodFractionOutsideAuction*float64(expectedTWAP)), fundingPayment.Int64()) 94 } 95 96 func testDataPointsInAuctionOutOfOrder(t *testing.T) { 97 perp := testPerpetual(t) 98 defer perp.ctrl.Finish() 99 expectedTWAP := 100 100 // set of the data points such that difference in averages is 0 101 points := getTestDataPoints(t) 102 103 // tell the perpetual that we are ready to accept settlement stuff 104 st := points[0].t 105 nd := points[3].t 106 a1 := between(points[0].t, points[1].t) 107 a2 := between(points[2].t, points[3].t) 108 109 whenLeaveOpeningAuction(t, perp, st) 110 111 // submit the first point and enter an auction 112 submitPointWithDifference(t, perp, points[0], expectedTWAP) 113 whenAuctionStateChanges(t, perp, a1, true) 114 whenAuctionStateChanges(t, perp, a2, false) 115 116 currentPeriodLength := float64(points[len(points)-1].t - points[0].t) 117 timeInAuction := float64(points[1].t - points[0].t + points[3].t - points[2].t) 118 periodFractionOutsideAuction := num.DecimalOne().Sub(num.DecimalFromFloat(timeInAuction).Div(num.DecimalFromFloat(currentPeriodLength))) 119 // funding payment will be the constant diff in the first point 120 expected, _ := num.IntFromDecimal(periodFractionOutsideAuction.Mul(num.DecimalFromInt64(100))) 121 assert.Equal(t, num.IntToString(expected), getFundingPayment(t, perp, nd)) 122 123 // now submit a point that is mid the auction period 124 submitPointWithDifference(t, perp, points[2], 200) 125 126 expected, _ = num.IntFromDecimal(periodFractionOutsideAuction.Mul(num.DecimalFromInt64(150))) 127 assert.Equal(t, num.IntToString(expected), getFundingPayment(t, perp, nd)) 128 129 // now submit a point also in before the previous point, also in an auction period 130 // and its contribution should be ignored. 131 crazy := &testDataPoint{t: between(a1, points[1].t), price: num.NewUint(1000)} 132 submitPointWithDifference(t, perp, crazy, 9999999) 133 assert.Equal(t, "49", getFundingPayment(t, perp, nd)) 134 } 135 136 func testAuctionFundingPeriodReset(t *testing.T) { 137 perp := testPerpetual(t) 138 defer perp.ctrl.Finish() 139 expectedTWAP := 100 140 // set of the data points such that difference in averages is 0 141 points := getTestDataPoints(t) 142 143 // tell the perpetual that we are ready to accept settlement stuff 144 whenLeaveOpeningAuction(t, perp, points[0].t) 145 146 // submit the first point and enter an auction 147 submitPointWithDifference(t, perp, points[0], expectedTWAP) 148 whenAuctionStateChanges(t, perp, points[0].t+int64(time.Second), true) 149 150 fundingPayment := whenTheFundingPeriodEnds(t, perp, points[0].t+int64(2*time.Second)) 151 periodFractionOutsideAuction := 0.5 152 assert.Equal(t, int64(periodFractionOutsideAuction*float64(expectedTWAP)), fundingPayment.Int64()) 153 154 // should still be on an auction to ending another funding period should give 0 155 submitPointWithDifference(t, perp, points[1], -999999) 156 fundingPayment = whenTheFundingPeriodEnds(t, perp, points[2].t) 157 assert.Equal(t, int64(0), fundingPayment.Int64()) 158 159 // submit a point and leave auction 160 submitPointWithDifference(t, perp, points[2], expectedTWAP) 161 whenAuctionStateChanges(t, perp, between(points[2].t, points[3].t), false) 162 163 fundingPayment = whenTheFundingPeriodEnds(t, perp, points[3].t) 164 assert.Equal(t, int64(periodFractionOutsideAuction*100), fundingPayment.Int64()) 165 166 // now we're not in an auction, ending the period again will preserve that 167 fundingPayment = whenTheFundingPeriodEnds(t, perp, points[3].t+int64(time.Hour)) 168 assert.Equal(t, int64(100), fundingPayment.Int64()) 169 } 170 171 func testFundingDataAtInAuctionPeriodStart(t *testing.T) { 172 perp := testPerpetual(t) 173 defer perp.ctrl.Finish() 174 expectedTWAP := 100 175 // set of the data points such that difference in averages is 0 176 points := getTestDataPoints(t) 177 178 // tell the perpetual that we are ready to accept settlement stuff 179 whenLeaveOpeningAuction(t, perp, points[0].t) 180 181 // submit the first point and enter an auction 182 submitPointWithDifference(t, perp, points[0], expectedTWAP) 183 whenAuctionStateChanges(t, perp, points[0].t+int64(time.Second), true) 184 185 end := points[0].t + int64(2*time.Second) 186 fundingPayment := whenTheFundingPeriodEnds(t, perp, end) 187 periodFractionOutsideAuction := 0.5 188 assert.Equal(t, int64(periodFractionOutsideAuction*float64(expectedTWAP)), fundingPayment.Int64()) 189 190 // but if we query the funding payment right now it'll be zero because this 0 length, just started 191 // funding period is all in auction 192 fp := getFundingPayment(t, perp, end) 193 assert.Equal(t, "0", fp) 194 } 195 196 func testPastFundingPaymentInAuction(t *testing.T) { 197 perp := testPerpetual(t) 198 defer perp.ctrl.Finish() 199 expectedTWAP := 100000000000 200 // set of the data points such that difference in averages is 0 201 points := getTestDataPoints(t) 202 203 // tell the perpetual that we are ready to accept settlement stuff 204 whenLeaveOpeningAuction(t, perp, points[0].t) 205 206 // submit the first point and enter an auction 207 submitPointWithDifference(t, perp, points[0], expectedTWAP) 208 209 // funding period ends so we have a carry-over 210 end := points[0].t + int64(2*time.Second) 211 fundingPayment := whenTheFundingPeriodEnds(t, perp, end) 212 assert.Equal(t, int64(expectedTWAP), fundingPayment.Int64()) 213 214 whenAuctionStateChanges(t, perp, points[1].t, true) 215 216 // now add another just an internal point 217 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 218 require.NoError(t, perp.perpetual.SubmitDataPoint(context.Background(), points[2].price, points[2].t)) 219 220 endPrev := end 221 end = points[2].t - int64(500*time.Millisecond) 222 fundingPayment = whenTheFundingPeriodEnds(t, perp, end) 223 224 currentPeriodLength := float64(end - endPrev) 225 timeInAuction := float64(end - points[1].t) 226 periodFractionOutsideAuction := 1 - timeInAuction/currentPeriodLength 227 228 assert.Equal(t, int64(periodFractionOutsideAuction*float64(expectedTWAP)), fundingPayment.Int64()) 229 } 230 231 func testPastFundingPayment(t *testing.T) { 232 perp := testPerpetual(t) 233 defer perp.ctrl.Finish() 234 expectedTWAP := 100000000000 235 // set of the data points such that difference in averages is 0 236 points := getTestDataPoints(t) 237 238 // tell the perpetual that we are ready to accept settlement stuff 239 whenLeaveOpeningAuction(t, perp, points[0].t) 240 241 // submit the first point and enter an auction 242 submitPointWithDifference(t, perp, points[0], expectedTWAP) 243 244 // funding period ends so we have a carry-over 245 end := points[0].t + int64(2*time.Second) 246 fundingPayment := whenTheFundingPeriodEnds(t, perp, end) 247 assert.Equal(t, int64(expectedTWAP), fundingPayment.Int64()) 248 249 // now add another just an internal point 250 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 251 require.NoError(t, perp.perpetual.SubmitDataPoint(context.Background(), points[2].price, points[2].t)) 252 253 end = points[2].t - int64(500*time.Millisecond) 254 fundingPayment = whenTheFundingPeriodEnds(t, perp, end) 255 assert.Equal(t, int64(expectedTWAP), fundingPayment.Int64()) 256 } 257 258 func TestZeroLengthAuctionPeriods(t *testing.T) { 259 perp := testPerpetual(t) 260 defer perp.ctrl.Finish() 261 262 // set of the data points such that difference in averages is 0 263 points := getTestDataPoints(t) 264 265 // tell the perpetual that we are ready to accept settlement stuff 266 whenLeaveOpeningAuction(t, perp, points[0].t) 267 268 // enter auction 269 whenAuctionStateChanges(t, perp, points[0].t, true) 270 271 // send in some data points with a TWAP difference 272 submitDataWithDifference(t, perp, points, 10) 273 274 // leave auction 275 whenAuctionStateChanges(t, perp, points[len(points)-1].t, false) 276 277 // but then enter again straight away 278 whenAuctionStateChanges(t, perp, points[len(points)-1].t, true) 279 280 fundingPayment := whenTheFundingPeriodEnds(t, perp, points[len(points)-1].t) 281 assert.Equal(t, "0", fundingPayment.String()) 282 } 283 284 func TestFairgroundPanic(t *testing.T) { 285 perp := testPerpetual(t) 286 defer perp.ctrl.Finish() 287 288 // tell the perpetual that we are ready to accept settlement stuff 289 whenLeaveOpeningAuction(t, perp, 1708097537000000000) 290 291 ctx := context.Background() 292 293 // submit the first point and enter an auction 294 perp.broker.EXPECT().Send(gomock.Any()).Times(4) 295 perp.perpetual.AddTestExternalPoint(ctx, num.NewUint(2375757190), 1706655048000000000) 296 perp.perpetual.SubmitDataPoint(ctx, num.NewUint(2375757190), 1706655048000000000) 297 298 // enter an auction 299 whenAuctionStateChanges(t, perp, 1708098633000000000, true) 300 301 // core asks for margin increase 302 perp.perpetual.GetMarginIncrease(1708098634815117249) 303 304 // then we leave auction in the same block 305 whenAuctionStateChanges(t, perp, 1708098634815117249, false) 306 307 // add a point 308 perp.perpetual.AddTestExternalPoint(ctx, num.NewUint(2375757190), 1708098648000000000) 309 310 // then add a older point 311 perp.perpetual.AddTestExternalPoint(ctx, num.NewUint(2375757190), 1708098612000000000) 312 } 313 314 func whenTheFundingPeriodEnds(t *testing.T, perp *tstPerp, now int64) *num.Int { 315 t.Helper() 316 ctx := context.Background() 317 var fundingPayment *num.Numeric 318 fn := func(_ context.Context, fp *num.Numeric) { 319 fundingPayment = fp 320 } 321 perp.perpetual.SetSettlementListener(fn) 322 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 323 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 324 perp.perpetual.PromptSettlementCue(ctx, now) 325 require.NotNil(t, fundingPayment) 326 require.True(t, fundingPayment.IsInt()) 327 return fundingPayment.Int() 328 } 329 330 func getFundingPayment(t *testing.T, perp *tstPerp, now int64) string { 331 t.Helper() 332 data := perp.perpetual.GetData(now).Data.(*types.PerpetualData) 333 return data.FundingPayment 334 } 335 336 func getFundingRate(t *testing.T, perp *tstPerp, now int64) string { 337 t.Helper() 338 data := perp.perpetual.GetData(now).Data.(*types.PerpetualData) 339 return data.FundingRate 340 } 341 342 func between(p, q int64) int64 { 343 return (p + q) / 2 344 }