From f1e8d981234f278037a3be96961e62ef83b88cb9 Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Wed, 27 Mar 2024 12:04:21 +0000 Subject: [PATCH] fix: account for active flag when sorting bounds Signed-off-by: Elias Van Ootegem --- core/monitor/price/snapshot.go | 10 ++- core/monitor/price/snapshot_test.go | 113 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/core/monitor/price/snapshot.go b/core/monitor/price/snapshot.go index 4d871d842bb..208e2a8477b 100644 --- a/core/monitor/price/snapshot.go +++ b/core/monitor/price/snapshot.go @@ -161,7 +161,8 @@ func newPriceRangeCacheFromSlice(prs []*types.PriceRangeCache) map[*bound]priceR return priceRangesCache } -func (e *Engine) serialisePriceRanges() []*types.PriceRangeCache { +// SerialisePriceranges expored for testing. +func (e *Engine) SerialisePriceRanges() []*types.PriceRangeCache { prc := make([]*types.PriceRangeCache, 0, len(e.priceRangesCache)) for bound, priceRange := range e.priceRangesCache { prc = append(prc, &types.PriceRangeCache{ @@ -174,7 +175,10 @@ func (e *Engine) serialisePriceRanges() []*types.PriceRangeCache { }) } - sort.Slice(prc, func(i, j int) bool { + sort.SliceStable(prc, func(i, j int) bool { + if prc[i].Bound.Active != prc[j].Bound.Active { + return prc[i].Bound.Active + } if prc[i].Bound.UpFactor.Equal(prc[j].Bound.UpFactor) { if prc[i].Bound.DownFactor.Equal(prc[j].Bound.DownFactor) { return prc[i].Bound.Trigger.Horizon < prc[j].Bound.Trigger.Horizon @@ -239,7 +243,7 @@ func (e *Engine) GetState() *types.PriceMonitor { Now: e.now, Update: e.update, Bounds: e.serialiseBounds(), - PriceRangeCache: e.serialisePriceRanges(), + PriceRangeCache: e.SerialisePriceRanges(), PricesNow: e.serialisePricesNow(), PricesPast: e.serialisePricesPast(), PriceRangeCacheTime: e.priceRangeCacheTime, diff --git a/core/monitor/price/snapshot_test.go b/core/monitor/price/snapshot_test.go index 0057f54eb77..67f0cc6eec8 100644 --- a/core/monitor/price/snapshot_test.go +++ b/core/monitor/price/snapshot_test.go @@ -206,3 +206,116 @@ func TestRestorePriceBoundRepresentation(t *testing.T) { require.Equal(t, min, sMin) require.Equal(t, max, sMax) } + +func TestSerialiseBoundsDeterministically(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + riskModel := mocks.NewMockRangeProvider(ctrl) + auctionStateMock := mocks.NewMockAuctionState(ctrl) + currentPrice := num.NewUint(123) + now := time.Date(1993, 2, 2, 6, 0, 0, 1, time.UTC) + + settings := types.PriceMonitoringSettingsFromProto(&vegapb.PriceMonitoringSettings{ + Parameters: &vegapb.PriceMonitoringParameters{ + Triggers: []*vegapb.PriceMonitoringTrigger{ + {Horizon: 3600, Probability: "0.99", AuctionExtension: 60}, + {Horizon: 3600, Probability: "0.99", AuctionExtension: 60}, + {Horizon: 3600, Probability: "0.99", AuctionExtension: 60}, + {Horizon: 3600, Probability: "0.99", AuctionExtension: 60}, + {Horizon: 3600, Probability: "0.99", AuctionExtension: 60}, + {Horizon: 7200, Probability: "0.95", AuctionExtension: 300}, + {Horizon: 7200, Probability: "0.95", AuctionExtension: 300}, + {Horizon: 7200, Probability: "0.95", AuctionExtension: 300}, + {Horizon: 7200, Probability: "0.95", AuctionExtension: 300}, + {Horizon: 7200, Probability: "0.95", AuctionExtension: 300}, + }, + }, + }) + + _, pMin1, pMax1, _, _ := getPriceBounds(currentPrice, 1, 2) + _, pMin2, pMax2, _, _ := getPriceBounds(currentPrice, 3, 4) + currentPriceD := currentPrice.ToDecimal() + auctionStateMock.EXPECT().IsFBA().Return(false).AnyTimes() + auctionStateMock.EXPECT().InAuction().Return(false).AnyTimes() + auctionStateMock.EXPECT().IsPriceAuction().Return(false).AnyTimes() + statevar := mocks.NewMockStateVarEngine(ctrl) + statevar.EXPECT().RegisterStateVariable(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + + pm, err := price.NewMonitor("asset", "market", riskModel, auctionStateMock, settings, statevar, logging.NewTestLogger()) + require.NoError(t, err) + require.NotNil(t, pm) + downFactors := []num.Decimal{ + pMin1.Div(currentPriceD), + pMin1.Div(currentPriceD), + pMin1.Div(currentPriceD), + pMin1.Div(currentPriceD), + pMin1.Div(currentPriceD), + pMin2.Div(currentPriceD), + pMin2.Div(currentPriceD), + pMin2.Div(currentPriceD), + pMin2.Div(currentPriceD), + pMin2.Div(currentPriceD), + } + upFactors := []num.Decimal{ + pMax1.Div(currentPriceD), + pMax1.Div(currentPriceD), + pMax1.Div(currentPriceD), + pMax1.Div(currentPriceD), + pMax1.Div(currentPriceD), + pMax2.Div(currentPriceD), + pMax2.Div(currentPriceD), + pMax2.Div(currentPriceD), + pMax2.Div(currentPriceD), + pMax2.Div(currentPriceD), + } + + pm.UpdateTestFactors(downFactors, upFactors) + + pm.OnTimeUpdate(now) + b := pm.CheckPrice(context.Background(), auctionStateMock, []*types.Trade{{Price: currentPrice, Size: 1}}, true) + require.False(t, b) + + bounds := pm.GetCurrentBounds() + require.NotEmpty(t, bounds) + minP := bounds[0].MinValidPrice.Clone() + minP.Sub(minP, num.UintOne()) + auctionStateMock.EXPECT().StartPriceAuction(gomock.Any(), gomock.Any()).Times(1) + b = pm.CheckPrice(context.Background(), auctionStateMock, []*types.Trade{{Price: minP, Size: 1}}, true) + require.False(t, b) + + pBounds := pm.SerialisePriceRanges() + // now get state + state := pm.GetState() + snap, err := price.NewMonitorFromSnapshot("market", "asset", state, settings, riskModel, auctionStateMock, statevar, logging.NewTestLogger()) + require.NoError(t, err) + + sBounds := snap.SerialisePriceRanges() + require.Equal(t, len(pBounds), len(sBounds)) + // ensure the inactive bound is at the back of the slice + require.False(t, pBounds[len(pBounds)-1].Bound.Active) + require.False(t, sBounds[len(sBounds)-1].Bound.Active) + for i := 0; i < len(sBounds); i++ { + pBound, sBound := pBounds[i], sBounds[i] + require.EqualValues(t, pBound, sBound) + } + // Now repeat the test above, but change the state to move the inactive bound back by one each time + for i := len(state.PriceRangeCache) - 1; i < 0; i-- { + // move the inactive price bound back by one + state.PriceRangeCache[i], state.PriceRangeCache[i-1] = state.PriceRangeCache[i-1], state.PriceRangeCache[i] + // sanity-check, make sure the inactive bound is now no longer the last element, and is where we expecti it to be + require.False(t, state.PriceRangeCache[i-1].Bound.Active) + require.True(t, state.PriceRangeCache[i].Bound.Active) + // always make sure the last element is active + require.True(t, state.PriceRangeCache[len(state.PriceRangeCache)-1].Bound.Active) + snap, err := price.NewMonitorFromSnapshot("market", "asset", state, settings, riskModel, auctionStateMock, statevar, logging.NewTestLogger()) + require.NoError(t, err) + sBounds := snap.SerialisePriceRanges() + require.Equal(t, len(pBounds), len(sBounds)) + // the inactive bound must be the last one + require.False(t, sBounds[len(sBounds)-1].Bound.Active) + for i := 0; i < len(sBounds); i++ { + pBound, sBound := pBounds[i], sBounds[i] + require.EqualValues(t, pBound, sBound) + } + } +}