code.vegaprotocol.io/vega@v0.79.0/core/liquidity/target/engine_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 target_test
    17  
    18  import (
    19  	"testing"
    20  	"time"
    21  
    22  	"code.vegaprotocol.io/vega/core/liquidity/target"
    23  	"code.vegaprotocol.io/vega/core/liquidity/target/mocks"
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	"code.vegaprotocol.io/vega/libs/num"
    26  
    27  	"github.com/golang/mock/gomock"
    28  	"github.com/stretchr/testify/assert"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  var (
    33  	now      = time.Date(2020, 10, 30, 9, 0, 0, 0, time.UTC)
    34  	marketID = "market-1"
    35  )
    36  
    37  func TestConstructor(t *testing.T) {
    38  	params := types.TargetStakeParameters{TimeWindow: 3600, ScalingFactor: num.DecimalFromFloat(10)}
    39  	engine := target.NewEngine(params, nil, marketID, num.DecimalFromFloat(1))
    40  
    41  	require.NotNil(t, engine)
    42  }
    43  
    44  func TestRecordOpenInterest(t *testing.T) {
    45  	params := types.TargetStakeParameters{TimeWindow: 3600, ScalingFactor: num.DecimalFromFloat(10)}
    46  	engine := target.NewEngine(params, nil, marketID, num.DecimalFromFloat(1))
    47  	err := engine.RecordOpenInterest(9, now)
    48  	require.NoError(t, err)
    49  	err = engine.RecordOpenInterest(0, now)
    50  	require.NoError(t, err)
    51  	err = engine.RecordOpenInterest(11, now.Add(time.Nanosecond))
    52  	require.NoError(t, err)
    53  	err = engine.RecordOpenInterest(12, now.Add(time.Nanosecond))
    54  	require.NoError(t, err)
    55  	err = engine.RecordOpenInterest(13, now.Add(-2*time.Nanosecond))
    56  	require.Error(t, err)
    57  }
    58  
    59  func TestGetTargetStake_NoRecordedOpenInterest(t *testing.T) {
    60  	params := types.TargetStakeParameters{TimeWindow: 3600, ScalingFactor: num.DecimalFromFloat(10)}
    61  	engine := target.NewEngine(params, nil, marketID, num.DecimalFromFloat(1))
    62  	rf := types.RiskFactor{
    63  		Long:  num.DecimalFromFloat(0.3),
    64  		Short: num.DecimalFromFloat(0.1),
    65  	}
    66  
    67  	targetStake, _ := engine.GetTargetStake(rf, now, num.NewUint(123))
    68  
    69  	require.Equal(t, num.UintZero(), targetStake)
    70  }
    71  
    72  func TestGetTargetStake_VerifyFormula(t *testing.T) {
    73  	tWindow := time.Hour
    74  	scalingFactor := num.DecimalFromFloat(11.3)
    75  	params := types.TargetStakeParameters{TimeWindow: int64(tWindow.Seconds()), ScalingFactor: scalingFactor}
    76  	rf := types.RiskFactor{
    77  		Long:  num.DecimalFromFloat(0.3),
    78  		Short: num.DecimalFromFloat(0.1),
    79  	}
    80  	oi := uint64(23)
    81  	markPrice := num.NewUint(123)
    82  
    83  	// float64(markPrice.Uint64()*oi) * math.Max(rfLong, rfShort) * scalingFactor
    84  	expectedTargetStake := num.DecimalFromUint(markPrice)
    85  	expectedTargetStake = expectedTargetStake.Mul(num.DecimalFromUint(num.NewUint(oi)))
    86  	expectedTargetStake = expectedTargetStake.Mul(rf.Long.Mul(scalingFactor))
    87  
    88  	engine := target.NewEngine(params, nil, marketID, num.DecimalFromFloat(1))
    89  
    90  	err := engine.RecordOpenInterest(oi, now)
    91  	require.NoError(t, err)
    92  
    93  	targetStakeNow, _ := engine.GetTargetStake(rf, now, markPrice.Clone())
    94  	targetStakeLaterInWindow, _ := engine.GetTargetStake(rf, now.Add(time.Minute), markPrice.Clone())
    95  	targetStakeAtEndOfWindow, _ := engine.GetTargetStake(rf, now.Add(tWindow), markPrice.Clone())
    96  	targetStakeAfterWindow, _ := engine.GetTargetStake(rf, now.Add(tWindow).Add(time.Nanosecond), markPrice.Clone())
    97  
    98  	expectedUint, _ := num.UintFromDecimal(expectedTargetStake)
    99  	require.Equal(t, expectedUint, targetStakeNow)
   100  	require.Equal(t, expectedUint, targetStakeLaterInWindow)
   101  	require.Equal(t, expectedUint, targetStakeAtEndOfWindow)
   102  	require.Equal(t, expectedUint, targetStakeAfterWindow)
   103  }
   104  
   105  func TestGetTargetStake_VerifyFormulaAfterParametersUpdate(t *testing.T) {
   106  	// given
   107  	tWindow := time.Hour
   108  	scalingFactor := num.DecimalFromFloat(11.3)
   109  	params := types.TargetStakeParameters{
   110  		TimeWindow:    int64(tWindow.Seconds()),
   111  		ScalingFactor: scalingFactor,
   112  	}
   113  	openInterest := uint64(23)
   114  
   115  	// setup
   116  	engine := target.NewEngine(params, nil, marketID, num.DecimalFromFloat(1))
   117  
   118  	// when
   119  	err := engine.RecordOpenInterest(openInterest, now)
   120  
   121  	// then
   122  	require.NoError(t, err)
   123  
   124  	// given
   125  	markPrice := num.NewUint(123)
   126  	rf := types.RiskFactor{
   127  		Long:  num.DecimalFromFloat(0.3),
   128  		Short: num.DecimalFromFloat(0.1),
   129  	}
   130  
   131  	// when
   132  	targetStakeNow, _ := engine.GetTargetStake(rf, now, markPrice.Clone())
   133  	targetStakeLaterInWindow, _ := engine.GetTargetStake(rf, now.Add(time.Minute), markPrice.Clone())
   134  	targetStakeAtEndOfWindow, _ := engine.GetTargetStake(rf, now.Add(tWindow), markPrice.Clone())
   135  	targetStakeAfterWindow, _ := engine.GetTargetStake(rf, now.Add(tWindow).Add(time.Nanosecond), markPrice.Clone())
   136  
   137  	// then
   138  	// float64(markPrice.Uint64()*openInterest) * math.Max(rf.Long, rf.Short) * scalingFactor
   139  	expectedTargetStake := num.DecimalFromUint(markPrice)
   140  	expectedTargetStake = expectedTargetStake.Mul(num.DecimalFromUint(num.NewUint(openInterest)))
   141  	expectedTargetStake = expectedTargetStake.Mul(rf.Long.Mul(scalingFactor))
   142  	expectedTargetStakeUint, _ := num.UintFromDecimal(expectedTargetStake)
   143  	assert.Equal(t, expectedTargetStakeUint, targetStakeNow)
   144  	assert.Equal(t, expectedTargetStakeUint, targetStakeLaterInWindow)
   145  	assert.Equal(t, expectedTargetStakeUint, targetStakeAtEndOfWindow)
   146  	assert.Equal(t, expectedTargetStakeUint, targetStakeAfterWindow)
   147  
   148  	// given
   149  	updatedTWindow := tWindow - (10 * time.Minute)
   150  	updatedParams := types.TargetStakeParameters{
   151  		TimeWindow:    int64(updatedTWindow.Seconds()),
   152  		ScalingFactor: num.DecimalFromFloat(10.5),
   153  	}
   154  
   155  	// when
   156  	engine.UpdateParameters(updatedParams)
   157  
   158  	// given
   159  
   160  	newOpenInterest := uint64(14)
   161  
   162  	// when
   163  	err = engine.RecordOpenInterest(newOpenInterest, now.Add(time.Second))
   164  
   165  	// when
   166  	require.NoError(t, err)
   167  
   168  	// The new open interest should be selected as a new max open interest,
   169  	// even though it's smaller than the previously registered open interest,
   170  	// because we are recording the new open interest a second after new
   171  	// maximum time an open interest is kept in memory.
   172  	later := now.Add(updatedTWindow).Add(2 * time.Second)
   173  
   174  	// when
   175  	updatedTargetStakeNow, _ := engine.GetTargetStake(rf, later, markPrice.Clone())
   176  	updatedTargetStakeLaterInWindow, _ := engine.GetTargetStake(rf, later.Add(time.Minute), markPrice.Clone())
   177  	updatedTargetStakeAtEndOfWindow, _ := engine.GetTargetStake(rf, later.Add(updatedTWindow), markPrice.Clone())
   178  	updatedTargetStakeAfterWindow, _ := engine.GetTargetStake(rf, later.Add(updatedTWindow).Add(time.Nanosecond), markPrice.Clone())
   179  
   180  	// then
   181  	// float64(markPrice.Uint64()*newOpenInterest) * math.Max(rfLong, rfShort) * updatedScalingFactor
   182  	expectedUpdatedTargetStake := num.DecimalFromUint(markPrice)
   183  	expectedUpdatedTargetStake = expectedUpdatedTargetStake.Mul(num.DecimalFromUint(num.NewUint(newOpenInterest)))
   184  	expectedUpdatedTargetStake = expectedUpdatedTargetStake.Mul(rf.Long.Mul(updatedParams.ScalingFactor))
   185  	expectedUpdatedTargetStakeUint, _ := num.UintFromDecimal(expectedUpdatedTargetStake)
   186  	assert.Equal(t, expectedUpdatedTargetStakeUint, updatedTargetStakeNow)
   187  	assert.Equal(t, expectedUpdatedTargetStakeUint, updatedTargetStakeLaterInWindow)
   188  	assert.Equal(t, expectedUpdatedTargetStakeUint, updatedTargetStakeAtEndOfWindow)
   189  	assert.Equal(t, expectedUpdatedTargetStakeUint, updatedTargetStakeAfterWindow)
   190  }
   191  
   192  func TestGetTargetStake_VerifyMaxOI(t *testing.T) {
   193  	tWindow := 60 * time.Minute
   194  	scalingFactor := num.DecimalFromFloat(11.3)
   195  	params := types.TargetStakeParameters{TimeWindow: int64(tWindow.Seconds()), ScalingFactor: scalingFactor}
   196  	rfLong := num.DecimalFromFloat(0.3)
   197  	rfShort := num.DecimalFromFloat(0.1)
   198  	markPrice := num.NewUint(123)
   199  	expectedTargetStake := func(oi uint64) *num.Uint {
   200  		// float64(markPrice.Uint64()*oi) * math.Max(rfLong, rfShort) * scalingFactor
   201  		mp := num.DecimalFromUint(markPrice)
   202  		mp = mp.Mul(num.DecimalFromUint(num.NewUint(oi)))
   203  		factor := rfLong
   204  		if factor.LessThan(rfShort) {
   205  			factor = rfShort
   206  		}
   207  		mp = mp.Mul(factor.Mul(scalingFactor))
   208  		ump, _ := num.UintFromDecimal(mp)
   209  		return ump
   210  	}
   211  
   212  	engine := target.NewEngine(params, nil, marketID, num.DecimalFromFloat(1))
   213  	rf := types.RiskFactor{
   214  		Long:  rfLong,
   215  		Short: rfShort,
   216  	}
   217  
   218  	// Max in current time
   219  	var maxOI uint64 = 23
   220  	err := engine.RecordOpenInterest(maxOI, now)
   221  	require.NoError(t, err)
   222  	actualTargetStake1, _ := engine.GetTargetStake(rf, now, markPrice.Clone())
   223  	actualTargetStake2, _ := engine.GetTargetStake(rf, now.Add(time.Minute), markPrice.Clone())
   224  
   225  	exp := expectedTargetStake(maxOI)
   226  	require.Equal(t, exp, actualTargetStake1)
   227  	require.Equal(t, exp, actualTargetStake2)
   228  	// Max in past
   229  	now = now.Add(time.Nanosecond)
   230  	markPrice = num.NewUint(456)
   231  	err = engine.RecordOpenInterest(maxOI-1, now)
   232  	require.NoError(t, err)
   233  	actualTargetStake1, _ = engine.GetTargetStake(rf, now, markPrice.Clone())
   234  	actualTargetStake2, _ = engine.GetTargetStake(rf, now.Add(time.Minute), markPrice.Clone())
   235  
   236  	exp = expectedTargetStake(maxOI)
   237  	require.Equal(t, exp, actualTargetStake1)
   238  	require.Equal(t, exp, actualTargetStake2)
   239  
   240  	// Max in current time
   241  	now = now.Add(time.Second)
   242  	maxOI = 10 * maxOI
   243  	markPrice = num.NewUint(23)
   244  	err = engine.RecordOpenInterest(maxOI, now)
   245  	require.NoError(t, err)
   246  	actualTargetStake1, _ = engine.GetTargetStake(rf, now, markPrice.Clone())
   247  	actualTargetStake2, _ = engine.GetTargetStake(rf, now.Add(time.Minute), markPrice.Clone())
   248  
   249  	exp = expectedTargetStake(maxOI)
   250  	require.Equal(t, exp, actualTargetStake1)
   251  	require.Equal(t, exp, actualTargetStake2)
   252  
   253  	// Max in past, move time beyond window, don't update OI, max OI should be the last recorded value
   254  	now = now.Add(time.Minute)
   255  	var lastRecordedValue uint64 = 1
   256  	err = engine.RecordOpenInterest(lastRecordedValue, now)
   257  	require.NoError(t, err)
   258  	now = now.Add(3 * tWindow)
   259  	markPrice = num.NewUint(7777777)
   260  	actualTargetStake1, _ = engine.GetTargetStake(rf, now, markPrice)
   261  	actualTargetStake2, _ = engine.GetTargetStake(rf, now.Add(time.Minute), markPrice)
   262  
   263  	exp = expectedTargetStake(lastRecordedValue)
   264  	require.Equal(t, exp, actualTargetStake1)
   265  	require.Equal(t, exp, actualTargetStake2)
   266  
   267  	// Max in past with smaller value after it, move time beyond window so that the current max gets dropped, now target stake should be based on next value
   268  	now = now.Add(time.Minute)
   269  	var penultimateValue uint64 = 1000
   270  	err = engine.RecordOpenInterest(penultimateValue, now)
   271  	require.NoError(t, err)
   272  	// Half a time window
   273  	now = now.Add(30 * time.Minute)
   274  	lastRecordedValue = 5
   275  	err = engine.RecordOpenInterest(lastRecordedValue, now)
   276  	require.NoError(t, err)
   277  	// Move entire time window and a bit
   278  	now = now.Add(61 * time.Minute)
   279  	markPrice = num.NewUint(7777777)
   280  	actualTargetStake1, _ = engine.GetTargetStake(rf, now, markPrice)
   281  	actualTargetStake2, _ = engine.GetTargetStake(rf, now.Add(time.Minute), markPrice)
   282  
   283  	exp = expectedTargetStake(lastRecordedValue)
   284  	require.Equal(t, exp, actualTargetStake1)
   285  	require.Equal(t, exp, actualTargetStake2)
   286  
   287  	// Max in past with OI of 0 value after it, move time beyond window so that the current max gets dropped, now target stake should be 0
   288  	now = now.Add(time.Minute)
   289  	penultimateValue = 1000
   290  	err = engine.RecordOpenInterest(penultimateValue, now)
   291  	require.NoError(t, err)
   292  	// Half a time window
   293  	now = now.Add(30 * time.Minute)
   294  	lastRecordedValueIsZero := uint64(0)
   295  	err = engine.RecordOpenInterest(lastRecordedValueIsZero, now)
   296  	require.NoError(t, err)
   297  	// Move entire time window and a bit
   298  	now = now.Add(61 * time.Minute)
   299  	markPrice = num.NewUint(7777777)
   300  	actualTargetStake1, _ = engine.GetTargetStake(rf, now, markPrice)
   301  	actualTargetStake2, _ = engine.GetTargetStake(rf, now.Add(time.Minute), markPrice)
   302  
   303  	exp = expectedTargetStake(lastRecordedValueIsZero)
   304  	require.Equal(t, exp, actualTargetStake1)
   305  	require.Equal(t, exp, actualTargetStake2)
   306  }
   307  
   308  func TestGetTheoreticalTargetStake(t *testing.T) {
   309  	tWindow := time.Hour
   310  	scalingFactor := num.DecimalFromFloat(11.3)
   311  	params := types.TargetStakeParameters{TimeWindow: int64(tWindow.Seconds()), ScalingFactor: scalingFactor}
   312  	rfLong := num.DecimalFromFloat(0.3)
   313  	rfShort := num.DecimalFromFloat(0.1)
   314  	var oi uint64 = 23
   315  	markPrice := num.NewUint(123)
   316  
   317  	factor := rfLong
   318  	if factor.LessThan(rfShort) {
   319  		factor = rfShort
   320  	}
   321  	expectedTargetStake, _ := num.UintFromDecimal(num.DecimalFromUint(markPrice).Mul(num.DecimalFromUint(num.NewUint(oi))).Mul(factor.Mul(scalingFactor)))
   322  
   323  	ctrl := gomock.NewController(t)
   324  	oiCalc := mocks.NewMockOpenInterestCalculator(ctrl)
   325  	engine := target.NewEngine(params, oiCalc, marketID, num.DecimalFromFloat(1))
   326  	rf := types.RiskFactor{
   327  		Long:  rfLong,
   328  		Short: rfShort,
   329  	}
   330  	err := engine.RecordOpenInterest(oi, now)
   331  	require.NoError(t, err)
   332  
   333  	targetStakeNow, _ := engine.GetTargetStake(rf, now, markPrice.Clone())
   334  	require.Equal(t, expectedTargetStake, targetStakeNow)
   335  
   336  	var trades []*types.Trade
   337  
   338  	// No change in OI
   339  	theoreticalOI := oi
   340  	oiCalc.EXPECT().GetOpenInterestGivenTrades(trades).Return(theoreticalOI).MaxTimes(1)
   341  	expectedTheoreticalTargetStake := expectedTargetStake.Clone()
   342  	theoreticalTargetStake, _ := engine.GetTheoreticalTargetStake(rf, now, markPrice.Clone(), trades)
   343  
   344  	require.Equal(t, expectedTheoreticalTargetStake, theoreticalTargetStake)
   345  
   346  	// OI decreases
   347  	theoreticalOI = oi - 2
   348  	oiCalc.EXPECT().GetOpenInterestGivenTrades(trades).Return(theoreticalOI).MaxTimes(1)
   349  	theoreticalTargetStake, _ = engine.GetTheoreticalTargetStake(rf, now, markPrice, trades)
   350  
   351  	require.Equal(t, expectedTheoreticalTargetStake, theoreticalTargetStake)
   352  
   353  	// OI increases
   354  	theoreticalOI = oi + 2
   355  	oiCalc.EXPECT().GetOpenInterestGivenTrades(trades).Return(theoreticalOI).MaxTimes(1)
   356  
   357  	expectedTheoreticalTargetStake, _ = num.UintFromDecimal(num.DecimalFromUint(markPrice).Mul(num.DecimalFromUint(num.NewUint(theoreticalOI))).Mul(factor.Mul(scalingFactor)))
   358  
   359  	theoreticalTargetStake, _ = engine.GetTheoreticalTargetStake(rf, now, markPrice, trades)
   360  
   361  	require.Equal(t, expectedTheoreticalTargetStake, theoreticalTargetStake)
   362  
   363  	// OI decreases
   364  	theoreticalOI = oi - 5
   365  	oiCalc.EXPECT().GetOpenInterestGivenTrades(trades).Return(theoreticalOI).MaxTimes(2)
   366  
   367  	now = now.Add(30 * time.Minute)
   368  	// last observation still within the time window so expecting theoretical target stake stay unchanged
   369  	theoreticalTargetStake, _ = engine.GetTheoreticalTargetStake(rf, now, markPrice, trades)
   370  	require.Equal(t, expectedTargetStake, theoreticalTargetStake)
   371  
   372  	// last observation out of the time window now so expecting theoretical target stake to drop
   373  	expectedTheoreticalTargetStake, _ = num.UintFromDecimal(num.DecimalFromUint(markPrice).Mul(num.DecimalFromUint(num.NewUint(theoreticalOI))).Mul(factor.Mul(scalingFactor)))
   374  	now = now.Add(31 * time.Minute)
   375  	theoreticalTargetStake, _ = engine.GetTheoreticalTargetStake(rf, now, markPrice, trades)
   376  	require.NotEqual(t, expectedTargetStake, theoreticalTargetStake)
   377  	require.True(t, expectedTheoreticalTargetStake.LT(expectedTargetStake))
   378  	require.Equal(t, expectedTheoreticalTargetStake, theoreticalTargetStake)
   379  }