code.vegaprotocol.io/vega@v0.79.0/core/products/perpetual_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 "encoding/json" 21 "fmt" 22 "math" 23 "sort" 24 "testing" 25 "time" 26 27 "code.vegaprotocol.io/vega/core/datasource" 28 dscommon "code.vegaprotocol.io/vega/core/datasource/common" 29 dstypes "code.vegaprotocol.io/vega/core/datasource/common" 30 "code.vegaprotocol.io/vega/core/datasource/external/signedoracle" 31 "code.vegaprotocol.io/vega/core/datasource/spec" 32 "code.vegaprotocol.io/vega/core/events" 33 "code.vegaprotocol.io/vega/core/products" 34 "code.vegaprotocol.io/vega/core/products/mocks" 35 "code.vegaprotocol.io/vega/core/types" 36 tmocks "code.vegaprotocol.io/vega/core/vegatime/mocks" 37 "code.vegaprotocol.io/vega/libs/num" 38 "code.vegaprotocol.io/vega/libs/ptr" 39 "code.vegaprotocol.io/vega/logging" 40 datapb "code.vegaprotocol.io/vega/protos/vega/data/v1" 41 42 "github.com/golang/mock/gomock" 43 "github.com/stretchr/testify/assert" 44 "github.com/stretchr/testify/require" 45 ) 46 47 func TestPeriodicSettlement(t *testing.T) { 48 t.Run("twap calculations after leaving opening auction", testTWAPAfterOpeningAuction) 49 t.Run("period end with no data point", testPeriodEndWithNoDataPoints) 50 t.Run("equal internal and external prices", testEqualInternalAndExternalPrices) 51 t.Run("constant difference long pays short", testConstantDifferenceLongPaysShort) 52 t.Run("data points outside of period", testDataPointsOutsidePeriod) 53 t.Run("data points not on boundary", testDataPointsNotOnBoundary) 54 t.Run("matching data points outside of period through callbacks", testRegisteredCallbacks) 55 t.Run("non-matching data points outside of period through callbacks", testRegisteredCallbacksWithDifferentData) 56 t.Run("funding payments with interest rate", testFundingPaymentsWithInterestRate) 57 t.Run("funding payments with interest rate clamped", testFundingPaymentsWithInterestRateClamped) 58 t.Run("terminate perps market test", testTerminateTrading) 59 t.Run("margin increase", testGetMarginIncrease) 60 t.Run("margin increase, negative payment", testGetMarginIncreaseNegativePayment) 61 t.Run("test pathological case with out of order points", testOutOfOrderPointsBeforePeriodStart) 62 t.Run("test update perpetual", testUpdatePerpetual) 63 t.Run("test terminate trading coincides with time trigger", testTerminateTradingCoincidesTimeTrigger) 64 t.Run("test funding-payment on start boundary", testFundingPaymentOnStartBoundary) 65 t.Run("test data point is before the first point", TestPrependPoint) 66 } 67 68 func TestRealData(t *testing.T) { 69 tcs := []struct { 70 name string 71 reverse bool 72 }{ 73 { 74 "in order", 75 false, 76 }, 77 { 78 "out of order", 79 false, 80 }, 81 } 82 83 for _, tc := range tcs { 84 t.Run(tc.name, func(tt *testing.T) { 85 perp := testPerpetual(t) 86 defer perp.ctrl.Finish() 87 88 ctx := context.Background() 89 tstData, err := getGQLData() 90 require.NoError(t, err) 91 data := tstData.GetDataPoints(false) 92 93 // want to start the period from before the point with the smallest time 94 seq := math.MaxInt 95 st := data[0].t 96 nd := data[0].t 97 for i := 0; i < len(data); i++ { 98 if data[i].t < st { 99 st = data[i].t 100 } 101 if data[i].t > nd { 102 nd = data[i].t 103 } 104 seq = num.MinV(seq, data[i].seq) 105 } 106 107 perp.perpetual.SetSettlementListener(func(context.Context, *num.Numeric) {}) 108 // leave opening auction 109 whenLeaveOpeningAuction(t, perp, st-1) 110 111 perp.broker.EXPECT().Send(gomock.Any()).AnyTimes() 112 perp.broker.EXPECT().SendBatch(gomock.Any()).AnyTimes() 113 114 // set the first internal data-point 115 for _, dp := range data { 116 if dp.seq > seq { 117 perp.perpetual.PromptSettlementCue(ctx, dp.t) 118 seq = dp.seq 119 } 120 perp.perpetual.AddTestExternalPoint(ctx, dp.price, dp.t) 121 perp.perpetual.SubmitDataPoint(ctx, num.UintZero().Add(dp.price, num.NewUint(100)), dp.t) 122 } 123 d := perp.perpetual.GetData(nd).Data.(*types.PerpetualData) 124 assert.Equal(t, "29124220000", d.ExternalTWAP) 125 assert.Equal(t, "29124220100", d.InternalTWAP) 126 assert.Equal(t, "100", d.FundingPayment) 127 }) 128 } 129 } 130 131 func testTWAPAfterOpeningAuction(t *testing.T) { 132 perp := testPerpetual(t) 133 defer perp.ctrl.Finish() 134 135 ctx := context.Background() 136 137 now := time.Unix(2000, 0).UnixNano() 138 139 // no error because its really a callback from the oracle engine, but we expect no events 140 perp.perpetual.AddTestExternalPoint(ctx, num.UintOne(), now) 141 data := perp.perpetual.GetData(2000) 142 require.Nil(t, data) 143 144 // internal data point recevied without error 145 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 146 err := perp.perpetual.SubmitDataPoint(ctx, num.NewUint(100000), now) 147 data = perp.perpetual.GetData(2000) 148 assert.NoError(t, err) 149 require.Nil(t, data) 150 151 // check that settlement cues are ignored, we expect no events when it is 152 perp.perpetual.PromptSettlementCue(ctx, 4000) 153 154 // now leaving opening auction 155 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 156 perp.ts.EXPECT().GetTimeNow().Times(1).Return(time.Unix(2000, 0)) 157 perp.perpetual.UpdateAuctionState(ctx, false) 158 159 // send in an external data-point which actually hits before opening auction time because it took a while to wobble through 160 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 161 perp.perpetual.AddTestExternalPoint(ctx, num.NewUint(100000), now-int64(time.Minute)) 162 fundingPayment := getFundingPayment(t, perp, now) 163 require.Equal(t, "0", fundingPayment) 164 165 // now another data point but both at the same time 166 dp := &testDataPoint{price: num.NewUint(200000), t: now + int64(time.Second)} 167 submitPointWithDifference(t, perp, dp, 0) 168 fundingPayment = getFundingPayment(t, perp, now+int64(time.Minute)) 169 require.Equal(t, "0", fundingPayment) 170 } 171 172 func testPeriodEndWithNoDataPoints(t *testing.T) { 173 perp := testPerpetual(t) 174 defer perp.ctrl.Finish() 175 176 ctx := context.Background() 177 now := time.Unix(1, 0) 178 179 // funding payment will be zero because there are no data points 180 var called bool 181 fn := func(context.Context, *num.Numeric) { 182 called = true 183 } 184 perp.perpetual.SetSettlementListener(fn) 185 186 whenLeaveOpeningAuction(t, perp, now.UnixNano()) 187 188 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 189 perp.perpetual.PromptSettlementCue(ctx, now.Add(40*time.Second).UnixNano()) 190 191 // we had no points to check we didn't call into listener 192 assert.False(t, called) 193 } 194 195 func TestPrependPoint(t *testing.T) { 196 perp := testPerpetual(t) 197 defer perp.ctrl.Finish() 198 199 ctx := context.Background() 200 now := time.Unix(1000, 0) 201 whenLeaveOpeningAuction(t, perp, now.UnixNano()) 202 203 perp.broker.EXPECT().Send(gomock.Any()).AnyTimes() 204 205 // we'll use this point to check that we do not lose a later point when we recalc when earlier points come in 206 err := perp.perpetual.SubmitDataPoint(ctx, num.NewUint(10), time.Unix(5000, 0).UnixNano()) 207 perp.perpetual.AddTestExternalPoint(ctx, num.NewUint(9), time.Unix(5000, 0).UnixNano()) 208 require.NoError(t, err) 209 require.Equal(t, "1", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 210 211 // first point is after the start of the period 212 err = perp.perpetual.SubmitDataPoint(ctx, num.NewUint(10), time.Unix(2000, 0).UnixNano()) 213 require.NoError(t, err) 214 require.Equal(t, "1", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 215 216 // now another one comes in before this, but also after the start of the period 217 err = perp.perpetual.SubmitDataPoint(ctx, num.NewUint(50), time.Unix(1500, 0).UnixNano()) 218 require.NoError(t, err) 219 require.Equal(t, "6", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 220 221 // now one comes in before the start of the period 222 err = perp.perpetual.SubmitDataPoint(ctx, num.NewUint(50), time.Unix(500, 0).UnixNano()) 223 require.NoError(t, err) 224 require.Equal(t, "11", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 225 226 // now one comes in before this point 227 err = perp.perpetual.SubmitDataPoint(ctx, num.UintOne(), time.Unix(250, 0).UnixNano()) 228 require.ErrorIs(t, err, products.ErrDataPointIsTooOld) 229 require.Equal(t, "11", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 230 231 // now one comes in after the first point, but before the period start 232 err = perp.perpetual.SubmitDataPoint(ctx, num.UintOne(), time.Unix(500, 0).UnixNano()) 233 require.ErrorIs(t, err, products.ErrDataPointAlreadyExistsAtTime) 234 require.Equal(t, "11", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 235 236 // now one comes in after the first point, but before the period start 237 err = perp.perpetual.SubmitDataPoint(ctx, num.NewUint(50), time.Unix(750, 0).UnixNano()) 238 require.NoError(t, err) 239 require.Equal(t, "11", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 240 241 // now one comes that equals period start 242 err = perp.perpetual.SubmitDataPoint(ctx, num.NewUint(50), time.Unix(1000, 0).UnixNano()) 243 require.NoError(t, err) 244 require.Equal(t, "11", getFundingPayment(t, perp, time.Unix(5000, 0).UnixNano())) 245 246 err = perp.perpetual.SubmitDataPoint(ctx, num.NewUint(100000), time.Unix(750, 0).UnixNano()) 247 require.ErrorIs(t, err, products.ErrDataPointIsTooOld) 248 } 249 250 func testEqualInternalAndExternalPrices(t *testing.T) { 251 perp := testPerpetual(t) 252 defer perp.ctrl.Finish() 253 ctx := context.Background() 254 255 // set of the data points such that difference in averages is 0 256 points := getTestDataPoints(t) 257 258 // tell the perpetual that we are ready to accept settlement stuff 259 whenLeaveOpeningAuction(t, perp, points[0].t) 260 261 // send in some data points 262 perp.broker.EXPECT().Send(gomock.Any()).Times(len(points) * 2) 263 for _, p := range points { 264 // send in an external and a matching internal 265 require.NoError(t, perp.perpetual.SubmitDataPoint(ctx, p.price, p.t)) 266 perp.perpetual.AddTestExternalPoint(ctx, p.price, p.t) 267 } 268 269 // ask for the funding payment 270 var fundingPayment *num.Numeric 271 fn := func(_ context.Context, fp *num.Numeric) { 272 fundingPayment = fp 273 } 274 perp.perpetual.SetSettlementListener(fn) 275 276 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 277 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 278 perp.perpetual.PromptSettlementCue(ctx, points[len(points)-1].t) 279 assert.NotNil(t, fundingPayment) 280 assert.True(t, fundingPayment.IsInt()) 281 assert.Equal(t, "0", fundingPayment.String()) 282 } 283 284 func testConstantDifferenceLongPaysShort(t *testing.T) { 285 perp := testPerpetual(t) 286 defer perp.ctrl.Finish() 287 ctx := context.Background() 288 289 // test data 290 points := getTestDataPoints(t) 291 292 // when: the funding period starts at 1000 293 whenLeaveOpeningAuction(t, perp, points[0].t) 294 295 // and: the difference in external/internal prices are a constant -10 296 submitDataWithDifference(t, perp, points, -10) 297 298 // funding payment will be zero so no transfers 299 var fundingPayment *num.Numeric 300 fn := func(_ context.Context, fp *num.Numeric) { 301 fundingPayment = fp 302 } 303 perp.perpetual.SetSettlementListener(fn) 304 305 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 306 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 307 308 productData := perp.perpetual.GetData(points[len(points)-1].t) 309 perpData, ok := productData.Data.(*types.PerpetualData) 310 assert.True(t, ok) 311 312 perp.perpetual.PromptSettlementCue(ctx, points[len(points)-1].t) 313 assert.NotNil(t, fundingPayment) 314 assert.True(t, fundingPayment.IsInt()) 315 assert.Equal(t, "-10", fundingPayment.String()) 316 assert.Equal(t, "-10", perpData.FundingPayment) 317 assert.Equal(t, "116", perpData.ExternalTWAP) 318 assert.Equal(t, "106", perpData.InternalTWAP) 319 assert.Equal(t, "-0.0862068965517241", perpData.FundingRate) 320 assert.Equal(t, uint64(0), perpData.SeqNum) 321 assert.Equal(t, int64(3600000000000), perpData.StartTime) 322 } 323 324 func testDataPointsOutsidePeriod(t *testing.T) { 325 perp := testPerpetual(t) 326 defer perp.ctrl.Finish() 327 ctx := context.Background() 328 329 // set of the data points such that difference in averages is 0 330 points := getTestDataPoints(t) 331 332 // tell the perpetual that we are ready to accept settlement stuff 333 whenLeaveOpeningAuction(t, perp, points[0].t) 334 335 // add data-points from the past, they will just be ignored 336 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 337 require.NoError(t, perp.perpetual.SubmitDataPoint(ctx, num.UintOne(), points[0].t-int64(time.Hour))) 338 perp.perpetual.AddTestExternalPoint(ctx, num.UintZero(), points[0].t-int64(time.Hour)) 339 340 // send in some data points 341 perp.broker.EXPECT().Send(gomock.Any()).Times(len(points) * 2) 342 for _, p := range points { 343 // send in an external and a matching internal 344 require.NoError(t, perp.perpetual.SubmitDataPoint(ctx, p.price, p.t)) 345 perp.perpetual.AddTestExternalPoint(ctx, p.price, p.t) 346 } 347 348 // add some data-points in the future from when we will cue the end of the funding period 349 // they should not affect the funding payment of this period 350 lastPoint := points[len(points)-1] 351 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 352 require.NoError(t, perp.perpetual.SubmitDataPoint(ctx, num.UintOne(), lastPoint.t+int64(time.Hour))) 353 perp.perpetual.AddTestExternalPoint(ctx, num.UintZero(), lastPoint.t+int64(time.Hour)) 354 355 // ask for the funding payment 356 var fundingPayment *num.Numeric 357 fn := func(_ context.Context, fp *num.Numeric) { 358 fundingPayment = fp 359 } 360 perp.perpetual.SetSettlementListener(fn) 361 362 // 6 times because: end + start of the period, plus 2 carry over points for external + internal (total 4) 363 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 364 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(func(evts []events.Event) { 365 require.Equal(t, 4, len(evts)) // 4 carry over points 366 }) 367 perp.perpetual.PromptSettlementCue(ctx, lastPoint.t) 368 assert.NotNil(t, fundingPayment) 369 assert.True(t, fundingPayment.IsInt()) 370 assert.Equal(t, "0", fundingPayment.String()) 371 } 372 373 func testDataPointsNotOnBoundary(t *testing.T) { 374 perp := testPerpetual(t) 375 defer perp.ctrl.Finish() 376 ctx := context.Background() 377 378 // set of the data points such that difference in averages is 0 379 points := getTestDataPoints(t) 380 381 // start time is *after* our first data points 382 whenLeaveOpeningAuction(t, perp, points[0].t+int64(time.Second)) 383 384 // send in some data points 385 submitDataWithDifference(t, perp, points, 10) 386 387 // ask for the funding payment 388 var fundingPayment *num.Numeric 389 fn := func(_ context.Context, fp *num.Numeric) { 390 fundingPayment = fp 391 } 392 perp.perpetual.SetSettlementListener(fn) 393 394 // period end is *after* our last point 395 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 396 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 397 perp.perpetual.PromptSettlementCue(ctx, points[len(points)-1].t+int64(time.Hour)) 398 assert.NotNil(t, fundingPayment) 399 assert.True(t, fundingPayment.IsInt()) 400 assert.Equal(t, "10", fundingPayment.String()) 401 } 402 403 func testOutOfOrderPointsBeforePeriodStart(t *testing.T) { 404 perp := testPerpetual(t) 405 defer perp.ctrl.Finish() 406 ctx := context.Background() 407 408 // start time will be after the *second* data point 409 whenLeaveOpeningAuction(t, perp, 1693398617000000000) 410 411 price := num.NewUint(100000000) 412 timestamps := []int64{ 413 1693398614000000000, 414 1693398615000000000, 415 1693398616000000000, 416 1693398618000000000, 417 1693398617000000000, 418 } 419 420 perp.broker.EXPECT().Send(gomock.Any()).AnyTimes() 421 perp.broker.EXPECT().SendBatch(gomock.Any()).AnyTimes() 422 for _, tt := range timestamps { 423 perp.perpetual.AddTestExternalPoint(ctx, price, tt) 424 perp.perpetual.SubmitDataPoint(ctx, num.UintZero().Add(price, num.NewUint(100000000)), tt) 425 } 426 427 // ask for the funding payment 428 var fundingPayment *num.Numeric 429 fn := func(_ context.Context, fp *num.Numeric) { 430 fundingPayment = fp 431 } 432 perp.perpetual.SetSettlementListener(fn) 433 434 // period end is *after* our last point 435 perp.perpetual.PromptSettlementCue(ctx, 1693398617000000000+int64(time.Hour)) 436 assert.NotNil(t, fundingPayment) 437 assert.True(t, fundingPayment.IsInt()) 438 assert.Equal(t, "100000000", fundingPayment.String()) 439 } 440 441 func testRegisteredCallbacks(t *testing.T) { 442 log := logging.NewTestLogger() 443 ctrl := gomock.NewController(t) 444 oe := mocks.NewMockOracleEngine(ctrl) 445 broker := mocks.NewMockBroker(ctrl) 446 ts := tmocks.NewMockTimeService(ctrl) 447 exp := &num.Numeric{} 448 exp.SetUint(num.UintZero()) 449 ctx := context.Background() 450 received := false 451 points := getTestDataPoints(t) 452 marketSettle := func(_ context.Context, data *num.Numeric) { 453 received = true 454 require.Equal(t, exp.String(), data.String()) 455 } 456 var settle, period spec.OnMatchedData 457 oe.EXPECT().Subscribe(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).DoAndReturn(func(_ context.Context, s spec.Spec, cb spec.OnMatchedData) (spec.SubscriptionID, spec.Unsubscriber, error) { 458 filters := s.OriginalSpec.GetDefinition().DataSourceType.GetFilters() 459 for _, f := range filters { 460 if f.Key.Type == datapb.PropertyKey_TYPE_INTEGER || f.Key.Type == datapb.PropertyKey_TYPE_DECIMAL { 461 settle = cb 462 return spec.SubscriptionID(1), func(_ context.Context, _ spec.SubscriptionID) {}, nil 463 } 464 } 465 period = cb 466 return spec.SubscriptionID(1), func(_ context.Context, _ spec.SubscriptionID) {}, nil 467 }) 468 broker.EXPECT().Send(gomock.Any()).AnyTimes() 469 broker.EXPECT().SendBatch(gomock.Any()).AnyTimes() 470 perp := getTestPerpProd(t) 471 perpetual, err := products.NewPerpetual(context.Background(), log, perp, "", ts, oe, broker, 1) 472 require.NoError(t, err) 473 require.NotNil(t, settle) 474 require.NotNil(t, period) 475 // register the callback 476 perpetual.NotifyOnSettlementData(marketSettle) 477 perpetual.NotifyOnDataSourcePropagation(func(context.Context, *num.Uint) {}) 478 479 ts.EXPECT().GetTimeNow().Times(1).Return(time.Unix(0, points[0].t)) 480 perpetual.UpdateAuctionState(ctx, false) 481 482 for _, p := range points { 483 // send in an external and a matching internal 484 require.NoError(t, perpetual.SubmitDataPoint(ctx, p.price, p.t)) 485 settle(ctx, dscommon.Data{ 486 Data: map[string]string{ 487 perp.DataSourceSpecBinding.SettlementDataProperty: p.price.String(), 488 }, 489 MetaData: map[string]string{ 490 "eth-block-time": fmt.Sprintf("%d", time.Unix(0, p.t).Unix()), 491 }, 492 }) 493 } 494 // add some data-points in the future from when we will cue the end of the funding period 495 // they should not affect the funding payment of this period 496 lastPoint := points[len(points)-1] 497 require.NoError(t, perpetual.SubmitDataPoint(ctx, num.UintOne(), lastPoint.t+int64(time.Hour))) 498 settle(ctx, dscommon.Data{ 499 Data: map[string]string{ 500 perp.DataSourceSpecBinding.SettlementDataProperty: "1", 501 }, 502 MetaData: map[string]string{ 503 "eth-block-time": fmt.Sprintf("%d", time.Unix(0, lastPoint.t+int64(time.Hour)).Unix()), 504 }, 505 }) 506 // make sure the data-point outside of the period doesn't trigger the schedule callback 507 // that has to come from the oracle, too 508 assert.False(t, received) 509 510 // end period 511 period(ctx, dscommon.Data{ 512 Data: map[string]string{ 513 perp.DataSourceSpecBinding.SettlementScheduleProperty: fmt.Sprintf("%d", time.Unix(0, lastPoint.t).Unix()), 514 }, 515 }) 516 517 assert.True(t, received) 518 } 519 520 func testRegisteredCallbacksWithDifferentData(t *testing.T) { 521 log := logging.NewTestLogger() 522 ctrl := gomock.NewController(t) 523 oe := mocks.NewMockOracleEngine(ctrl) 524 broker := mocks.NewMockBroker(ctrl) 525 ts := tmocks.NewMockTimeService(ctrl) 526 exp := &num.Numeric{} 527 // should be 2 528 res, _ := num.IntFromString("-4", 10) 529 exp.SetInt(res) 530 ctx := context.Background() 531 received := false 532 points := getTestDataPoints(t) 533 marketSettle := func(_ context.Context, data *num.Numeric) { 534 received = true 535 require.Equal(t, exp.String(), data.String()) 536 } 537 var settle, period spec.OnMatchedData 538 oe.EXPECT().Subscribe(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).DoAndReturn(func(_ context.Context, s spec.Spec, cb spec.OnMatchedData) (spec.SubscriptionID, spec.Unsubscriber, error) { 539 filters := s.OriginalSpec.GetDefinition().DataSourceType.GetFilters() 540 for _, f := range filters { 541 if f.Key.Type == datapb.PropertyKey_TYPE_INTEGER || f.Key.Type == datapb.PropertyKey_TYPE_DECIMAL { 542 settle = cb 543 return spec.SubscriptionID(1), func(_ context.Context, _ spec.SubscriptionID) {}, nil 544 } 545 } 546 period = cb 547 return spec.SubscriptionID(1), func(_ context.Context, _ spec.SubscriptionID) {}, nil 548 }) 549 broker.EXPECT().Send(gomock.Any()).AnyTimes() 550 broker.EXPECT().SendBatch(gomock.Any()).AnyTimes() 551 perp := getTestPerpProd(t) 552 perpetual, err := products.NewPerpetual(context.Background(), log, perp, "", ts, oe, broker, 1) 553 require.NoError(t, err) 554 require.NotNil(t, settle) 555 require.NotNil(t, period) 556 // register the callback 557 perpetual.NotifyOnSettlementData(marketSettle) 558 perpetual.NotifyOnDataSourcePropagation(func(context.Context, *num.Uint) {}) 559 560 // start the funding period 561 ts.EXPECT().GetTimeNow().Times(1).Return(time.Unix(0, points[0].t)) 562 perpetual.UpdateAuctionState(ctx, false) 563 564 // send data in from before the start of the period, it should not affect the result 565 require.NoError(t, perpetual.SubmitDataPoint(ctx, num.UintOne(), points[0].t-int64(time.Hour))) 566 // callback to receive settlement data 567 settle(ctx, dscommon.Data{ 568 Data: map[string]string{ 569 perp.DataSourceSpecBinding.SettlementDataProperty: "1", 570 }, 571 MetaData: map[string]string{ 572 "eth-block-time": fmt.Sprintf("%d", time.Unix(0, points[0].t-int64(time.Hour)).Unix()), 573 }, 574 }) 575 576 // send all external points, but not all internal ones and have their price 577 // be one less. This means external twap > internal tswap so we expect a negative funding rate 578 for i, p := range points { 579 if i%2 == 0 { 580 ip := num.UintZero().Sub(p.price, num.UintOne()) 581 require.NoError(t, perpetual.SubmitDataPoint(ctx, ip, p.t)) 582 } 583 settle(ctx, dscommon.Data{ 584 Data: map[string]string{ 585 perp.DataSourceSpecBinding.SettlementDataProperty: p.price.String(), 586 }, 587 MetaData: map[string]string{ 588 "eth-block-time": fmt.Sprintf("%d", time.Unix(0, p.t).Unix()), 589 }, 590 }) 591 } 592 593 // add some data-points in the future from when we will cue the end of the funding period 594 // they should not affect the funding payment of this period 595 lastPoint := points[len(points)-1] 596 require.NoError(t, perpetual.SubmitDataPoint(ctx, num.UintOne(), lastPoint.t+int64(time.Hour))) 597 settle(ctx, dscommon.Data{ 598 Data: map[string]string{ 599 perp.DataSourceSpecBinding.SettlementDataProperty: "1", 600 }, 601 MetaData: map[string]string{ 602 "eth-block-time": fmt.Sprintf("%d", time.Unix(0, lastPoint.t+int64(time.Hour)).Unix()), 603 }, 604 }) 605 606 // end period 607 period(ctx, dscommon.Data{ 608 Data: map[string]string{ 609 perp.DataSourceSpecBinding.SettlementScheduleProperty: fmt.Sprintf("%d", time.Unix(0, lastPoint.t).Unix()), 610 }, 611 }) 612 613 assert.True(t, received) 614 } 615 616 func testFundingPaymentsWithInterestRate(t *testing.T) { 617 perp := testPerpetual(t) 618 perp.perp.InterestRate = num.DecimalFromFloat(0.01) 619 perp.perp.ClampLowerBound = num.DecimalFromInt64(-1) 620 perp.perp.ClampUpperBound = num.DecimalFromInt64(1) 621 622 defer perp.ctrl.Finish() 623 ctx := context.Background() 624 625 // test data 626 points := getTestDataPoints(t) 627 lastPoint := points[len(points)-1] 628 629 // when: the funding period starts 630 whenLeaveOpeningAuction(t, perp, points[0].t) 631 632 // scale the price so that we have more precision to work with 633 scale := num.UintFromUint64(100000000000) 634 for _, p := range points { 635 p.price = num.UintZero().Mul(p.price, scale) 636 } 637 638 // and: the difference in external/internal prices are a constant -10 639 submitDataWithDifference(t, perp, points, -1000000000000) 640 641 // Whats happening: 642 // the fundingPayment without the interest terms will be -10000000000000 643 // 644 // interest will be (1 + r * t) * swap - fswap 645 // where r = 0.01, t = 0.25, stwap = 11666666666666, ftwap = 10666666666656, 646 // interest = (1 + 0.0025) * 11666666666666 - 10666666666656 = 1029166666666 647 648 // since lower clamp < interest < upper clamp 649 // -11666666666666 < 1029166666666 < 11666666666666 650 // there is no adjustment and so 651 // funding payment = -10000000000000 + 1029166666666 = 29166666666 652 653 var fundingPayment *num.Numeric 654 fn := func(_ context.Context, fp *num.Numeric) { 655 fundingPayment = fp 656 } 657 perp.perpetual.SetSettlementListener(fn) 658 659 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 660 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 661 perp.perpetual.PromptSettlementCue(ctx, lastPoint.t) 662 assert.NotNil(t, fundingPayment) 663 assert.True(t, fundingPayment.IsInt()) 664 assert.Equal(t, "29166666666", fundingPayment.String()) 665 } 666 667 func testFundingPaymentsWithInterestRateClamped(t *testing.T) { 668 perp := testPerpetual(t) 669 defer perp.ctrl.Finish() 670 perp.perp.InterestRate = num.DecimalFromFloat(0.5) 671 perp.perp.ClampLowerBound = num.DecimalFromFloat(0.001) 672 perp.perp.ClampUpperBound = num.DecimalFromFloat(0.002) 673 ctx := context.Background() 674 675 // test data 676 points := getTestDataPoints(t) 677 678 // when: the funding period starts 679 whenLeaveOpeningAuction(t, perp, points[0].t) 680 681 // scale the price so that we have more precision to work with 682 scale := num.UintFromUint64(100000000000) 683 for _, p := range points { 684 p.price = num.UintZero().Mul(p.price, scale) 685 } 686 687 // and: the difference in external/internal prices are a constant -10 688 submitDataWithDifference(t, perp, points, -10) 689 690 // Whats happening: 691 // the fundingPayment will be -10 without the interest terms 692 // 693 // interest will be (1 + r * t) * swap - fswap 694 // where stwap=116, ftwap=106, r=0.5 t=0.25 695 // interest = (1 + 0.125) * 11666666666666 - 11666666666656 = 1458333333343 696 697 // if we consider the clamps: 698 // lower clamp: 11666666666 699 // interest: 1458333333343 700 // upper clamp: 23333333333 701 702 // so we have exceeded the upper clamp the the interest term is snapped to it and so: 703 // funding payment = -10 + 23333333333 = 23333333323 704 705 var fundingPayment *num.Numeric 706 fn := func(_ context.Context, fp *num.Numeric) { 707 fundingPayment = fp 708 } 709 perp.perpetual.SetSettlementListener(fn) 710 711 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 712 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 713 perp.perpetual.PromptSettlementCue(ctx, points[3].t) 714 assert.NotNil(t, fundingPayment) 715 assert.True(t, fundingPayment.IsInt()) 716 assert.Equal(t, "23333333323", fundingPayment.String()) 717 } 718 719 func testTerminateTrading(t *testing.T) { 720 perp := testPerpetual(t) 721 defer perp.ctrl.Finish() 722 ctx := context.Background() 723 724 // set of the data points such that difference in averages is 0 725 points := getTestDataPoints(t) 726 727 // tell the perpetual that we are ready to accept settlement stuff 728 whenLeaveOpeningAuction(t, perp, points[0].t) 729 730 // send in some data points 731 perp.broker.EXPECT().Send(gomock.Any()).Times(len(points) * 2) 732 for _, p := range points { 733 // send in an external and a matching internal 734 require.NoError(t, perp.perpetual.SubmitDataPoint(ctx, p.price, p.t)) 735 perp.perpetual.AddTestExternalPoint(ctx, p.price, p.t) 736 } 737 738 // ask for the funding payment 739 var fundingPayment *num.Numeric 740 fn := func(_ context.Context, fp *num.Numeric) { 741 fundingPayment = fp 742 } 743 perp.perpetual.SetSettlementListener(fn) 744 745 perp.ts.EXPECT().GetTimeNow().Times(1).Return(time.Unix(10, points[len(points)-1].t)) 746 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 747 perp.perpetual.UnsubscribeTradingTerminated(ctx) 748 assert.NotNil(t, fundingPayment) 749 assert.True(t, fundingPayment.IsInt()) 750 assert.Equal(t, "0", fundingPayment.String()) 751 } 752 753 func testTerminateTradingCoincidesTimeTrigger(t *testing.T) { 754 perp := testPerpetual(t) 755 defer perp.ctrl.Finish() 756 ctx := context.Background() 757 758 // set of the data points such that difference in averages is 0 759 points := getTestDataPoints(t) 760 761 // tell the perpetual that we are ready to accept settlement stuff 762 whenLeaveOpeningAuction(t, perp, points[0].t) 763 764 // send in some data points 765 perp.broker.EXPECT().Send(gomock.Any()).Times(len(points) * 2) 766 for _, p := range points { 767 // send in an external and a matching internal 768 require.NoError(t, perp.perpetual.SubmitDataPoint(ctx, p.price, p.t)) 769 perp.perpetual.AddTestExternalPoint(ctx, p.price, p.t) 770 } 771 772 // ask for the funding payment 773 var fundingPayment *num.Numeric 774 fn := func(_ context.Context, fp *num.Numeric) { 775 fundingPayment = fp 776 } 777 perp.perpetual.SetSettlementListener(fn) 778 779 // do a normal settlement cue end time 780 endTime := time.Unix(10, points[len(points)-1].t).Truncate(time.Second) 781 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 782 perp.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 783 perp.perpetual.PromptSettlementCue(ctx, endTime.UnixNano()) 784 assert.NotNil(t, fundingPayment) 785 assert.True(t, fundingPayment.IsInt()) 786 assert.Equal(t, "0", fundingPayment.String()) 787 788 // now terminate the market at the same time, we expect no funding payment, and just an event 789 // to say the period has ended, with no start period. 790 fundingPayment = nil 791 perp.ts.EXPECT().GetTimeNow().Times(1).Return(time.Unix(10, points[len(points)-1].t)) 792 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 793 perp.perpetual.UnsubscribeTradingTerminated(ctx) 794 assert.Nil(t, fundingPayment) 795 } 796 797 func testGetMarginIncrease(t *testing.T) { 798 perp := testPerpetual(t) 799 defer perp.ctrl.Finish() 800 perp.perp.MarginFundingFactor = num.DecimalFromFloat(0.5) 801 802 // test data 803 points := getTestDataPoints(t) 804 805 // before we've started the first funding interval margin increase is 0 806 inc := perp.perpetual.GetMarginIncrease(points[0].t) 807 assert.Equal(t, "0", inc.String()) 808 809 // start funding period 810 whenLeaveOpeningAuction(t, perp, points[0].t) 811 812 // started interval, but not points, margin increase is 0 813 inc = perp.perpetual.GetMarginIncrease(points[0].t) 814 assert.Equal(t, "0", inc.String()) 815 816 // and: the difference in external/internal prices are is 10 817 submitDataWithDifference(t, perp, points, 10) 818 819 lastPoint := points[len(points)-1] 820 inc = perp.perpetual.GetMarginIncrease(lastPoint.t) 821 // margin increase is margin_factor * funding-payment = 0.5 * 10 822 assert.Equal(t, "5", inc.String()) 823 } 824 825 func testGetMarginIncreaseNegativePayment(t *testing.T) { 826 perp := testPerpetual(t) 827 defer perp.ctrl.Finish() 828 perp.perp.MarginFundingFactor = num.DecimalFromFloat(0.5) 829 830 // test data 831 points := getTestDataPoints(t) 832 833 // start funding period 834 whenLeaveOpeningAuction(t, perp, points[0].t) 835 836 // and: the difference in external/internal prices are is 10 837 submitDataWithDifference(t, perp, points, -10) 838 839 lastPoint := points[len(points)-1] 840 inc := perp.perpetual.GetMarginIncrease(lastPoint.t) 841 // margin increase is margin_factor * funding-payment = 0.5 * 10 842 assert.Equal(t, "-5", inc.String()) 843 } 844 845 func testUpdatePerpetual(t *testing.T) { 846 perp := testPerpetual(t) 847 defer perp.ctrl.Finish() 848 perp.perp.MarginFundingFactor = num.DecimalFromFloat(0.5) 849 ctx := context.Background() 850 851 // test data 852 points := getTestDataPoints(t) 853 whenLeaveOpeningAuction(t, perp, points[0].t) 854 submitDataWithDifference(t, perp, points, 10) 855 856 // query margin factor before update 857 lastPoint := points[len(points)-1] 858 inc := perp.perpetual.GetMarginIncrease(lastPoint.t) 859 assert.Equal(t, "5", inc.String()) 860 861 // do the perps update with a new margin factor 862 update := getTestPerpProd(t) 863 update.MarginFundingFactor = num.DecimalFromFloat(1) 864 err := perp.perpetual.Update(ctx, &types.InstrumentPerps{Perps: update}, perp.oe) 865 require.NoError(t, err) 866 867 // expect two unsubscriptions 868 assert.Equal(t, perp.unsub, 2) 869 870 // margin increase should now be double, which means the data-points were preserved 871 inc = perp.perpetual.GetMarginIncrease(lastPoint.t) 872 assert.Equal(t, "10", inc.String()) 873 874 // now submit a data point and check it is expected i.e the funding period is still active 875 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 876 assert.NoError(t, perp.perpetual.SubmitDataPoint(ctx, num.NewUint(123), lastPoint.t+int64(time.Hour))) 877 } 878 879 func testFundingPaymentOnStartBoundary(t *testing.T) { 880 perp := testPerpetual(t) 881 defer perp.ctrl.Finish() 882 // set of the data points such that difference in averages is 0 883 points := getTestDataPoints(t) 884 885 // tell the perpetual that we are ready to accept settlement stuff 886 st := points[0].t 887 whenLeaveOpeningAuction(t, perp, st) 888 889 expectedTWAP := 100 890 // send in data points at this time 891 submitPointWithDifference(t, perp, points[0], expectedTWAP) 892 893 // now get the funding-payment at this time 894 fundingPayment := getFundingPayment(t, perp, st) 895 assert.Equal(t, "100", fundingPayment) 896 } 897 898 func TestFundingPaymentModifiers(t *testing.T) { 899 cases := []struct { 900 twapDifference int 901 scalingFactor *num.Decimal 902 upperBound *num.Decimal 903 lowerBound *num.Decimal 904 expectedFundingPayment string 905 expectedFundingRate string 906 }{ 907 { 908 twapDifference: 220, 909 scalingFactor: ptr.From(num.DecimalFromFloat(0.5)), 910 expectedFundingPayment: "110", 911 expectedFundingRate: "1", 912 }, 913 { 914 twapDifference: 1100, 915 scalingFactor: ptr.From(num.DecimalFromFloat(1.5)), 916 expectedFundingPayment: "1650", 917 expectedFundingRate: "15", 918 }, 919 { 920 twapDifference: 100, 921 upperBound: ptr.From(num.DecimalFromFloat(0.5)), 922 expectedFundingPayment: "55", // 0.5 * external-twap < diff, so snap to 0.5 923 expectedFundingRate: "0.5", 924 }, 925 { 926 twapDifference: 5, 927 lowerBound: ptr.From(num.DecimalFromFloat(0.5)), 928 expectedFundingPayment: "55", // 0.5 * external-twap > 5, so snap to 0.5 929 expectedFundingRate: "0.5", 930 }, 931 { 932 twapDifference: 1100, 933 scalingFactor: ptr.From(num.DecimalFromFloat(1.5)), 934 upperBound: ptr.From(num.DecimalFromFloat(0.5)), 935 expectedFundingPayment: "55", 936 expectedFundingRate: "0.5", 937 }, 938 } 939 940 for _, c := range cases { 941 perp := testPerpetual(t) 942 defer perp.ctrl.Finish() 943 944 // set modifiers 945 perp.perp.FundingRateScalingFactor = c.scalingFactor 946 perp.perp.FundingRateLowerBound = c.lowerBound 947 perp.perp.FundingRateUpperBound = c.upperBound 948 949 // tell the perpetual that we are ready to accept settlement stuff 950 points := getTestDataPoints(t) 951 whenLeaveOpeningAuction(t, perp, points[0].t) 952 submitPointWithDifference(t, perp, points[0], c.twapDifference) 953 954 // check the goods 955 fundingPayment := getFundingPayment(t, perp, points[0].t) 956 assert.Equal(t, c.expectedFundingPayment, fundingPayment) 957 958 fundingRate := getFundingRate(t, perp, points[0].t) 959 assert.Equal(t, c.expectedFundingRate, fundingRate) 960 } 961 } 962 963 // submits the given data points as both external and interval but with the given different added to the internal price. 964 func submitDataWithDifference(t *testing.T, perp *tstPerp, points []*testDataPoint, diff int) { 965 t.Helper() 966 for _, p := range points { 967 submitPointWithDifference(t, perp, p, diff) 968 } 969 } 970 971 // submits the single data point as both external and internal but with a differece in price. 972 func submitPointWithDifference(t *testing.T, perp *tstPerp, p *testDataPoint, diff int) { 973 t.Helper() 974 ctx := context.Background() 975 976 var internalPrice *num.Uint 977 perp.broker.EXPECT().Send(gomock.Any()).Times(2) 978 perp.perpetual.AddTestExternalPoint(ctx, p.price, p.t) 979 internalPrice = p.price.Clone() 980 981 if diff > 0 { 982 internalPrice = num.UintZero().Add(p.price, num.NewUint(uint64(diff))) 983 } 984 if diff < 0 { 985 internalPrice = num.UintZero().Sub(p.price, num.NewUint(uint64(-diff))) 986 } 987 require.NoError(t, perp.perpetual.SubmitDataPoint(ctx, internalPrice, p.t)) 988 } 989 990 func whenLeaveOpeningAuction(t *testing.T, perp *tstPerp, now int64) { 991 t.Helper() 992 perp.broker.EXPECT().Send(gomock.Any()).Times(1) 993 whenAuctionStateChanges(t, perp, now, false) 994 } 995 996 func whenAuctionStateChanges(t *testing.T, perp *tstPerp, now int64, enter bool) { 997 t.Helper() 998 perp.ts.EXPECT().GetTimeNow().Times(1).Return(time.Unix(0, now)) 999 perp.perpetual.UpdateAuctionState(context.Background(), enter) 1000 } 1001 1002 type testDataPoint struct { 1003 price *num.Uint 1004 t int64 1005 } 1006 1007 func getTestDataPoints(t *testing.T) []*testDataPoint { 1008 t.Helper() 1009 1010 // interest-rates are daily so we want the time of the data-points to be of that scale 1011 // so we make them over 6 hours, a quarter of a day. 1012 1013 year := 31536000000000000 1014 month := int64(year / 12) 1015 st := int64(time.Hour) 1016 1017 return []*testDataPoint{ 1018 { 1019 price: num.NewUint(110), 1020 t: st, 1021 }, 1022 { 1023 price: num.NewUint(120), 1024 t: st + month, 1025 }, 1026 { 1027 price: num.NewUint(120), 1028 t: st + (month * 2), 1029 }, 1030 { 1031 price: num.NewUint(100), 1032 t: st + (month * 3), 1033 }, 1034 } 1035 } 1036 1037 type tstPerp struct { 1038 oe *mocks.MockOracleEngine 1039 ts *tmocks.MockTimeService 1040 broker *mocks.MockBroker 1041 perpetual *products.Perpetual 1042 ctrl *gomock.Controller 1043 perp *types.Perps 1044 1045 unsub int 1046 } 1047 1048 func (tp *tstPerp) unsubscribe(_ context.Context, _ spec.SubscriptionID) { 1049 tp.unsub++ 1050 } 1051 1052 func testPerpetual(t *testing.T) *tstPerp { 1053 t.Helper() 1054 1055 log := logging.NewTestLogger() 1056 ctrl := gomock.NewController(t) 1057 oe := mocks.NewMockOracleEngine(ctrl) 1058 broker := mocks.NewMockBroker(ctrl) 1059 ts := tmocks.NewMockTimeService(ctrl) 1060 perp := getTestPerpProd(t) 1061 1062 tp := &tstPerp{ 1063 ts: ts, 1064 oe: oe, 1065 broker: broker, 1066 ctrl: ctrl, 1067 perp: perp, 1068 } 1069 tp.oe.EXPECT().Subscribe(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(spec.SubscriptionID(1), tp.unsubscribe, nil) 1070 1071 perpetual, err := products.NewPerpetual(context.Background(), log, perp, "", ts, oe, broker, 1) 1072 if err != nil { 1073 t.Fatalf("couldn't create a perp for testing: %v", err) 1074 } 1075 1076 perpetual.NotifyOnDataSourcePropagation(func(context.Context, *num.Uint) {}) 1077 1078 tp.perpetual = perpetual 1079 return tp 1080 } 1081 1082 func getTestPerpProd(t *testing.T) *types.Perps { 1083 t.Helper() 1084 dp := uint32(1) 1085 pubKeys := []*dstypes.Signer{ 1086 dstypes.CreateSignerFromString("0xDEADBEEF", dstypes.SignerTypePubKey), 1087 } 1088 1089 factor, _ := num.DecimalFromString("0.5") 1090 settlementSrc := &datasource.Spec{ 1091 Data: datasource.NewDefinition( 1092 datasource.ContentTypeOracle, 1093 ).SetOracleConfig( 1094 &signedoracle.SpecConfiguration{ 1095 Signers: pubKeys, 1096 Filters: []*dstypes.SpecFilter{ 1097 { 1098 Key: &dstypes.SpecPropertyKey{ 1099 Name: "foo", 1100 Type: datapb.PropertyKey_TYPE_INTEGER, 1101 NumberDecimalPlaces: ptr.From(uint64(dp)), 1102 }, 1103 Conditions: nil, 1104 }, 1105 }, 1106 }, 1107 ), 1108 } 1109 1110 scheduleSrc := &datasource.Spec{ 1111 Data: datasource.NewDefinition( 1112 datasource.ContentTypeOracle, 1113 ).SetOracleConfig(&signedoracle.SpecConfiguration{ 1114 Signers: pubKeys, 1115 Filters: []*dstypes.SpecFilter{ 1116 { 1117 Key: &dstypes.SpecPropertyKey{ 1118 Name: "bar", 1119 Type: datapb.PropertyKey_TYPE_TIMESTAMP, 1120 }, 1121 Conditions: nil, 1122 }, 1123 }, 1124 }), 1125 } 1126 1127 return &types.Perps{ 1128 MarginFundingFactor: factor, 1129 DataSourceSpecForSettlementData: settlementSrc, 1130 DataSourceSpecForSettlementSchedule: scheduleSrc, 1131 DataSourceSpecBinding: &datasource.SpecBindingForPerps{ 1132 SettlementDataProperty: "foo", 1133 SettlementScheduleProperty: "bar", 1134 }, 1135 } 1136 } 1137 1138 type DataPoint struct { 1139 price *num.Uint 1140 t int64 1141 seq int 1142 twap *num.Uint 1143 } 1144 1145 type FundingNode struct { 1146 Timestamp time.Time `json:"timestamp"` 1147 Seq int `json:"seq"` 1148 Price string `json:"price"` 1149 TWAP string `json:"twap"` 1150 Source string `json:"dataPointSource"` 1151 } 1152 1153 type Edge struct { 1154 Node FundingNode `json:"node"` 1155 } 1156 1157 type FundingDataPoints struct { 1158 Edges []Edge `json:"edges"` 1159 } 1160 1161 type GQLData struct { 1162 FundingDataPoints FundingDataPoints `json:"fundingPeriodDataPoints"` 1163 } 1164 1165 type GQL struct { 1166 Data GQLData `json:"data"` 1167 } 1168 1169 const testData = `{ 1170 "data": { 1171 "fundingPeriodDataPoints": { 1172 "edges": [ 1173 { 1174 "node": { 1175 "timestamp": "2023-08-16T13:52:00Z", 1176 "seq": 6, 1177 "price": "29124220000", 1178 "twap": "29124220000", 1179 "dataPointSource": "SOURCE_EXTERNAL" 1180 } 1181 }, 1182 { 1183 "node": { 1184 "timestamp": "2023-08-16T13:51:36Z", 1185 "seq": 6, 1186 "price": "29124220000", 1187 "twap": "29124220000", 1188 "dataPointSource": "SOURCE_EXTERNAL" 1189 } 1190 }, 1191 { 1192 "node": { 1193 "timestamp": "2023-08-16T13:51:00Z", 1194 "seq": 6, 1195 "price": "29124220000", 1196 "twap": "29124220000", 1197 "dataPointSource": "SOURCE_EXTERNAL" 1198 } 1199 }, 1200 { 1201 "node": { 1202 "timestamp": "2023-08-16T13:50:36Z", 1203 "seq": 6, 1204 "price": "29124220000", 1205 "twap": "29124220000", 1206 "dataPointSource": "SOURCE_EXTERNAL" 1207 } 1208 }, 1209 { 1210 "node": { 1211 "timestamp": "2023-08-16T13:50:00Z", 1212 "seq": 6, 1213 "price": "29124220000", 1214 "twap": "29124220000", 1215 "dataPointSource": "SOURCE_EXTERNAL" 1216 } 1217 }, 1218 { 1219 "node": { 1220 "timestamp": "2023-08-16T13:49:36Z", 1221 "seq": 6, 1222 "price": "29124220000", 1223 "twap": "29124220000", 1224 "dataPointSource": "SOURCE_EXTERNAL" 1225 } 1226 }, 1227 { 1228 "node": { 1229 "timestamp": "2023-08-16T13:49:00Z", 1230 "seq": 5, 1231 "price": "29124220000", 1232 "twap": "29124220000", 1233 "dataPointSource": "SOURCE_EXTERNAL" 1234 } 1235 }, 1236 { 1237 "node": { 1238 "timestamp": "2023-08-16T13:48:36Z", 1239 "seq": 5, 1240 "price": "29124220000", 1241 "twap": "29124220000", 1242 "dataPointSource": "SOURCE_EXTERNAL" 1243 } 1244 }, 1245 { 1246 "node": { 1247 "timestamp": "2023-08-16T13:48:12Z", 1248 "seq": 5, 1249 "price": "29124220000", 1250 "twap": "29124220000", 1251 "dataPointSource": "SOURCE_EXTERNAL" 1252 } 1253 }, 1254 { 1255 "node": { 1256 "timestamp": "2023-08-16T13:47:36Z", 1257 "seq": 5, 1258 "price": "29124220000", 1259 "twap": "29124220000", 1260 "dataPointSource": "SOURCE_EXTERNAL" 1261 } 1262 }, 1263 { 1264 "node": { 1265 "timestamp": "2023-08-16T13:47:00Z", 1266 "seq": 5, 1267 "price": "29124220000", 1268 "twap": "29124220000", 1269 "dataPointSource": "SOURCE_EXTERNAL" 1270 } 1271 }, 1272 { 1273 "node": { 1274 "timestamp": "2023-08-16T13:46:36Z", 1275 "seq": 5, 1276 "price": "29124220000", 1277 "twap": "29124220000", 1278 "dataPointSource": "SOURCE_EXTERNAL" 1279 } 1280 }, 1281 { 1282 "node": { 1283 "timestamp": "2023-08-16T13:46:00Z", 1284 "seq": 5, 1285 "price": "29124220000", 1286 "twap": "29124220000", 1287 "dataPointSource": "SOURCE_EXTERNAL" 1288 } 1289 }, 1290 { 1291 "node": { 1292 "timestamp": "2023-08-16T13:45:36Z", 1293 "seq": 5, 1294 "price": "29124220000", 1295 "twap": "29124220000", 1296 "dataPointSource": "SOURCE_EXTERNAL" 1297 } 1298 }, 1299 { 1300 "node": { 1301 "timestamp": "2023-08-16T13:45:00Z", 1302 "seq": 5, 1303 "price": "29124220000", 1304 "twap": "29124220000", 1305 "dataPointSource": "SOURCE_EXTERNAL" 1306 } 1307 }, 1308 { 1309 "node": { 1310 "timestamp": "2023-08-16T13:44:36Z", 1311 "seq": 5, 1312 "price": "29124220000", 1313 "twap": "29124220000", 1314 "dataPointSource": "SOURCE_EXTERNAL" 1315 } 1316 }, 1317 { 1318 "node": { 1319 "timestamp": "2023-08-16T13:44:00Z", 1320 "seq": 4, 1321 "price": "29124220000", 1322 "twap": "29124220000", 1323 "dataPointSource": "SOURCE_EXTERNAL" 1324 } 1325 }, 1326 { 1327 "node": { 1328 "timestamp": "2023-08-16T13:43:36Z", 1329 "seq": 4, 1330 "price": "29124220000", 1331 "twap": "29124220000", 1332 "dataPointSource": "SOURCE_EXTERNAL" 1333 } 1334 }, 1335 { 1336 "node": { 1337 "timestamp": "2023-08-16T13:43:00Z", 1338 "seq": 4, 1339 "price": "29124220000", 1340 "twap": "29124220000", 1341 "dataPointSource": "SOURCE_EXTERNAL" 1342 } 1343 }, 1344 { 1345 "node": { 1346 "timestamp": "2023-08-16T13:42:36Z", 1347 "seq": 4, 1348 "price": "29124220000", 1349 "twap": "29124220000", 1350 "dataPointSource": "SOURCE_EXTERNAL" 1351 } 1352 }, 1353 { 1354 "node": { 1355 "timestamp": "2023-08-16T13:42:00Z", 1356 "seq": 4, 1357 "price": "29124220000", 1358 "twap": "29124220000", 1359 "dataPointSource": "SOURCE_EXTERNAL" 1360 } 1361 }, 1362 { 1363 "node": { 1364 "timestamp": "2023-08-16T13:41:36Z", 1365 "seq": 4, 1366 "price": "29124220000", 1367 "twap": "29124220000", 1368 "dataPointSource": "SOURCE_EXTERNAL" 1369 } 1370 }, 1371 { 1372 "node": { 1373 "timestamp": "2023-08-16T13:41:24Z", 1374 "seq": 4, 1375 "price": "29124220000", 1376 "twap": "29124220000", 1377 "dataPointSource": "SOURCE_EXTERNAL" 1378 } 1379 }, 1380 { 1381 "node": { 1382 "timestamp": "2023-08-16T13:40:36Z", 1383 "seq": 4, 1384 "price": "29124220000", 1385 "twap": "29124220000", 1386 "dataPointSource": "SOURCE_EXTERNAL" 1387 } 1388 }, 1389 { 1390 "node": { 1391 "timestamp": "2023-08-16T13:40:00Z", 1392 "seq": 4, 1393 "price": "29124220000", 1394 "twap": "29124220000", 1395 "dataPointSource": "SOURCE_EXTERNAL" 1396 } 1397 }, 1398 { 1399 "node": { 1400 "timestamp": "2023-08-16T13:39:36Z", 1401 "seq": 4, 1402 "price": "29124220000", 1403 "twap": "29124220000", 1404 "dataPointSource": "SOURCE_EXTERNAL" 1405 } 1406 }, 1407 { 1408 "node": { 1409 "timestamp": "2023-08-16T13:39:12Z", 1410 "seq": 4, 1411 "price": "29124220000", 1412 "twap": "29124220000", 1413 "dataPointSource": "SOURCE_EXTERNAL" 1414 } 1415 }, 1416 { 1417 "node": { 1418 "timestamp": "2023-08-16T13:39:12Z", 1419 "seq": 3, 1420 "price": "29124220000", 1421 "twap": "29124220000", 1422 "dataPointSource": "SOURCE_EXTERNAL" 1423 } 1424 }, 1425 { 1426 "node": { 1427 "timestamp": "2023-08-16T13:38:48Z", 1428 "seq": 4, 1429 "price": "29124220000", 1430 "twap": "29124220000", 1431 "dataPointSource": "SOURCE_EXTERNAL" 1432 } 1433 }, 1434 { 1435 "node": { 1436 "timestamp": "2023-08-16T13:38:12Z", 1437 "seq": 3, 1438 "price": "29124220000", 1439 "twap": "29124220000", 1440 "dataPointSource": "SOURCE_EXTERNAL" 1441 } 1442 }, 1443 { 1444 "node": { 1445 "timestamp": "2023-08-16T13:37:36Z", 1446 "seq": 3, 1447 "price": "29124220000", 1448 "twap": "29124220000", 1449 "dataPointSource": "SOURCE_EXTERNAL" 1450 } 1451 }, 1452 { 1453 "node": { 1454 "timestamp": "2023-08-16T13:37:00Z", 1455 "seq": 3, 1456 "price": "29124220000", 1457 "twap": "29124220000", 1458 "dataPointSource": "SOURCE_EXTERNAL" 1459 } 1460 }, 1461 { 1462 "node": { 1463 "timestamp": "2023-08-16T13:36:36Z", 1464 "seq": 3, 1465 "price": "29124220000", 1466 "twap": "29124220000", 1467 "dataPointSource": "SOURCE_EXTERNAL" 1468 } 1469 }, 1470 { 1471 "node": { 1472 "timestamp": "2023-08-16T13:36:00Z", 1473 "seq": 3, 1474 "price": "29124220000", 1475 "twap": "29124220000", 1476 "dataPointSource": "SOURCE_EXTERNAL" 1477 } 1478 }, 1479 { 1480 "node": { 1481 "timestamp": "2023-08-16T13:35:36Z", 1482 "seq": 3, 1483 "price": "29124220000", 1484 "twap": "29124220000", 1485 "dataPointSource": "SOURCE_EXTERNAL" 1486 } 1487 }, 1488 { 1489 "node": { 1490 "timestamp": "2023-08-16T13:35:00Z", 1491 "seq": 3, 1492 "price": "29124220000", 1493 "twap": "29124220000", 1494 "dataPointSource": "SOURCE_EXTERNAL" 1495 } 1496 }, 1497 { 1498 "node": { 1499 "timestamp": "2023-08-16T13:34:36Z", 1500 "seq": 3, 1501 "price": "29124220000", 1502 "twap": "29124220000", 1503 "dataPointSource": "SOURCE_EXTERNAL" 1504 } 1505 }, 1506 { 1507 "node": { 1508 "timestamp": "2023-08-16T13:34:00Z", 1509 "seq": 2, 1510 "price": "29124220000", 1511 "twap": "29124220000", 1512 "dataPointSource": "SOURCE_EXTERNAL" 1513 } 1514 }, 1515 { 1516 "node": { 1517 "timestamp": "2023-08-16T13:33:36Z", 1518 "seq": 2, 1519 "price": "29124220000", 1520 "twap": "29124220000", 1521 "dataPointSource": "SOURCE_EXTERNAL" 1522 } 1523 }, 1524 { 1525 "node": { 1526 "timestamp": "2023-08-16T13:33:00Z", 1527 "seq": 2, 1528 "price": "29124220000", 1529 "twap": "29124220000", 1530 "dataPointSource": "SOURCE_EXTERNAL" 1531 } 1532 }, 1533 { 1534 "node": { 1535 "timestamp": "2023-08-16T13:32:36Z", 1536 "seq": 2, 1537 "price": "29124220000", 1538 "twap": "29124220000", 1539 "dataPointSource": "SOURCE_EXTERNAL" 1540 } 1541 }, 1542 { 1543 "node": { 1544 "timestamp": "2023-08-16T13:32:00Z", 1545 "seq": 2, 1546 "price": "29124220000", 1547 "twap": "29124220000", 1548 "dataPointSource": "SOURCE_EXTERNAL" 1549 } 1550 }, 1551 { 1552 "node": { 1553 "timestamp": "2023-08-16T13:31:48Z", 1554 "seq": 2, 1555 "price": "29124220000", 1556 "twap": "29124220000", 1557 "dataPointSource": "SOURCE_EXTERNAL" 1558 } 1559 }, 1560 { 1561 "node": { 1562 "timestamp": "2023-08-16T13:31:00Z", 1563 "seq": 2, 1564 "price": "29124220000", 1565 "twap": "29124220000", 1566 "dataPointSource": "SOURCE_EXTERNAL" 1567 } 1568 }, 1569 { 1570 "node": { 1571 "timestamp": "2023-08-16T13:30:36Z", 1572 "seq": 2, 1573 "price": "29124220000", 1574 "twap": "29124220000", 1575 "dataPointSource": "SOURCE_EXTERNAL" 1576 } 1577 }, 1578 { 1579 "node": { 1580 "timestamp": "2023-08-16T13:30:00Z", 1581 "seq": 2, 1582 "price": "29124220000", 1583 "twap": "29124220000", 1584 "dataPointSource": "SOURCE_EXTERNAL" 1585 } 1586 }, 1587 { 1588 "node": { 1589 "timestamp": "2023-08-16T13:29:36Z", 1590 "seq": 2, 1591 "price": "29124220000", 1592 "twap": "29124220000", 1593 "dataPointSource": "SOURCE_EXTERNAL" 1594 } 1595 }, 1596 { 1597 "node": { 1598 "timestamp": "2023-08-16T13:29:00Z", 1599 "seq": 1, 1600 "price": "29124220000", 1601 "twap": "29124220000", 1602 "dataPointSource": "SOURCE_EXTERNAL" 1603 } 1604 }, 1605 { 1606 "node": { 1607 "timestamp": "2023-08-16T13:28:36Z", 1608 "seq": 1, 1609 "price": "29124220000", 1610 "twap": "29124220000", 1611 "dataPointSource": "SOURCE_EXTERNAL" 1612 } 1613 }, 1614 { 1615 "node": { 1616 "timestamp": "2023-08-16T13:28:00Z", 1617 "seq": 1, 1618 "price": "29124220000", 1619 "twap": "29124220000", 1620 "dataPointSource": "SOURCE_EXTERNAL" 1621 } 1622 }, 1623 { 1624 "node": { 1625 "timestamp": "2023-08-16T13:27:36Z", 1626 "seq": 1, 1627 "price": "29124220000", 1628 "twap": "29124220000", 1629 "dataPointSource": "SOURCE_EXTERNAL" 1630 } 1631 }, 1632 { 1633 "node": { 1634 "timestamp": "2023-08-16T13:27:12Z", 1635 "seq": 1, 1636 "price": "29124220000", 1637 "twap": "29124220000", 1638 "dataPointSource": "SOURCE_EXTERNAL" 1639 } 1640 }, 1641 { 1642 "node": { 1643 "timestamp": "2023-08-16T13:26:36Z", 1644 "seq": 1, 1645 "price": "29124220000", 1646 "twap": "29124220000", 1647 "dataPointSource": "SOURCE_EXTERNAL" 1648 } 1649 }, 1650 { 1651 "node": { 1652 "timestamp": "2023-08-16T13:26:00Z", 1653 "seq": 1, 1654 "price": "29124220000", 1655 "twap": "29124220000", 1656 "dataPointSource": "SOURCE_EXTERNAL" 1657 } 1658 }, 1659 { 1660 "node": { 1661 "timestamp": "2023-08-16T13:25:36Z", 1662 "seq": 1, 1663 "price": "29124220000", 1664 "twap": "29124220000", 1665 "dataPointSource": "SOURCE_EXTERNAL" 1666 } 1667 }, 1668 { 1669 "node": { 1670 "timestamp": "2023-08-16T13:25:12Z", 1671 "seq": 1, 1672 "price": "29124220000", 1673 "twap": "29124220000", 1674 "dataPointSource": "SOURCE_EXTERNAL" 1675 } 1676 }, 1677 { 1678 "node": { 1679 "timestamp": "2023-08-16T13:24:48Z", 1680 "seq": 1, 1681 "price": "29124220000", 1682 "twap": "29124220000", 1683 "dataPointSource": "SOURCE_EXTERNAL" 1684 } 1685 }, 1686 { 1687 "node": { 1688 "timestamp": "2023-08-16T13:24:00Z", 1689 "seq": 1, 1690 "price": "29124220000", 1691 "twap": "29124220000", 1692 "dataPointSource": "SOURCE_EXTERNAL" 1693 } 1694 } 1695 ] 1696 } 1697 } 1698 }` 1699 1700 func getGQLData() (*GQL, error) { 1701 ret := GQL{} 1702 if err := json.Unmarshal([]byte(testData), &ret); err != nil { 1703 return nil, err 1704 } 1705 return &ret, nil 1706 } 1707 1708 func (g *GQL) Sort(reverse bool) { 1709 // group by sequence 1710 sort.SliceStable(g.Data.FundingDataPoints.Edges, func(i, j int) bool { 1711 if g.Data.FundingDataPoints.Edges[i].Node.Seq == g.Data.FundingDataPoints.Edges[j].Node.Seq { 1712 if reverse { 1713 return g.Data.FundingDataPoints.Edges[i].Node.Timestamp.UnixNano() > g.Data.FundingDataPoints.Edges[j].Node.Timestamp.UnixNano() 1714 } 1715 return g.Data.FundingDataPoints.Edges[i].Node.Timestamp.UnixNano() < g.Data.FundingDataPoints.Edges[j].Node.Timestamp.UnixNano() 1716 } 1717 1718 return g.Data.FundingDataPoints.Edges[i].Node.Seq < g.Data.FundingDataPoints.Edges[j].Node.Seq 1719 }) 1720 } 1721 1722 func (g *GQL) GetDataPoints(reverse bool) []DataPoint { 1723 g.Sort(reverse) 1724 ret := make([]DataPoint, 0, len(g.Data.FundingDataPoints.Edges)) 1725 for _, n := range g.Data.FundingDataPoints.Edges { 1726 p, _ := num.UintFromString(n.Node.Price, 10) 1727 twap, _ := num.UintFromString(n.Node.TWAP, 10) 1728 ret = append(ret, DataPoint{ 1729 price: p, 1730 t: n.Node.Timestamp.UnixNano(), 1731 seq: n.Node.Seq, 1732 twap: twap, 1733 }) 1734 } 1735 return ret 1736 }