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 }