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  }