From 907189cb2d08958a12cc2c79e3764c06b3a4d772 Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Wed, 18 Sep 2024 16:51:46 +0100 Subject: [PATCH 1/7] fix: recalculate stats when the program is updated Signed-off-by: Elias Van Ootegem --- CHANGELOG.md | 1 + core/referral/engine.go | 15 +++++++--- core/referral/snapshot_test.go | 48 ++++++++++++++++++++++-------- core/volumediscount/engine.go | 9 ++++++ core/volumediscount/engine_test.go | 2 ++ core/volumerebate/engine.go | 6 ++++ core/volumerebate/engine_test.go | 18 ++++++++--- 7 files changed, 78 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 271a382b778..625290cc09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - [11672](https://github.com/vegaprotocol/vega/issues/11672) - Add missing fees in GraphQL bindings. - [11681](https://github.com/vegaprotocol/vega/issues/11681) - Account for conflicts inserting funding payment records. - [11696](https://github.com/vegaprotocol/vega/issues/11696) - Add binding for estimate fees API. +- [11699](https://github.com/vegaprotocol/vega/issues/11699) - Update factors of programs when they are updated. ## 0.78.2 diff --git a/core/referral/engine.go b/core/referral/engine.go index 0022f37219c..7b9bcba4f6a 100644 --- a/core/referral/engine.go +++ b/core/referral/engine.go @@ -339,10 +339,14 @@ func (e *Engine) OnReferralProgramMaxPartyNotionalVolumeByQuantumPerEpochUpdate( func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { switch ep.Action { case vegapb.EpochAction_EPOCH_ACTION_START: + pp := e.currentProgram e.currentEpoch = ep.Seq e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) + if pp != nil && pp != e.currentProgram && !e.programHasEnded { + e.computeReferralSetsStats(ctx, ep, false) + } case vegapb.EpochAction_EPOCH_ACTION_END: - e.computeReferralSetsStats(ctx, ep) + e.computeReferralSetsStats(ctx, ep, true) } } @@ -495,7 +499,7 @@ func (e *Engine) loadReferralSetsFromSnapshot(setsProto []*snapshotpb.ReferralSe } } -func (e *Engine) computeReferralSetsStats(ctx context.Context, epoch types.Epoch) { +func (e *Engine) computeReferralSetsStats(ctx context.Context, epoch types.Epoch, sendEvents bool) { priorEpoch := uint64(0) if epoch.Seq > MaximumWindowLength { priorEpoch = epoch.Seq - MaximumWindowLength @@ -522,10 +526,10 @@ func (e *Engine) computeReferralSetsStats(ctx context.Context, epoch types.Epoch return } - e.computeFactorsByReferee(ctx, epoch.Seq, takerVolumePerReferee, referrersTakerVolume) + e.computeFactorsByReferee(ctx, epoch.Seq, takerVolumePerReferee, referrersTakerVolume, sendEvents) } -func (e *Engine) computeFactorsByReferee(ctx context.Context, epoch uint64, takerVolumePerReferee, referrersTakesVolume map[types.PartyID]*num.Uint) { +func (e *Engine) computeFactorsByReferee(ctx context.Context, epoch uint64, takerVolumePerReferee, referrersTakesVolume map[types.PartyID]*num.Uint, sendEvents bool) { e.factorsByReferee = map[types.PartyID]*types.RefereeStats{} allStats := map[types.ReferralSetID]*types.ReferralSetStats{} @@ -595,6 +599,9 @@ func (e *Engine) computeFactorsByReferee(ctx context.Context, epoch uint64, take } } + if !sendEvents { + return + } setIDs := maps.Keys(allStats) slices.Sort(setIDs) for _, setID := range setIDs { diff --git a/core/referral/snapshot_test.go b/core/referral/snapshot_test.go index 5ff48d38045..140a2618603 100644 --- a/core/referral/snapshot_test.go +++ b/core/referral/snapshot_test.go @@ -91,19 +91,41 @@ func TestTakingAndRestoringSnapshotSucceeds(t *testing.T) { // Simulating end of epoch. // The program should be applied. - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer1)).Return(num.UintFromUint64(10)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer2)).Return(num.UintFromUint64(20)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer3)).Return(num.UintFromUint64(30)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer4)).Return(num.UintFromUint64(40)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee1)).Return(num.UintFromUint64(50)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee2)).Return(num.UintFromUint64(60)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee3)).Return(num.UintFromUint64(70)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee4)).Return(num.UintFromUint64(80)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee5)).Return(num.UintFromUint64(90)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee6)).Return(num.UintFromUint64(100)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee7)).Return(num.UintFromUint64(110)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee8)).Return(num.UintFromUint64(120)).Times(1) - te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee9)).Return(num.UintFromUint64(130)).Times(1) + epochEndVals := map[string]*num.Uint{ + string(referrer1): num.UintFromUint64(10), + string(referrer2): num.UintFromUint64(20), + string(referrer3): num.UintFromUint64(30), + string(referrer4): num.UintFromUint64(40), + string(referee1): num.UintFromUint64(50), + string(referee2): num.UintFromUint64(60), + string(referee3): num.UintFromUint64(70), + string(referee4): num.UintFromUint64(80), + string(referee5): num.UintFromUint64(90), + string(referee6): num.UintFromUint64(100), + string(referee7): num.UintFromUint64(110), + string(referee8): num.UintFromUint64(120), + string(referee9): num.UintFromUint64(130), + } + te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(gomock.Any()).DoAndReturn(func(k string) *num.Uint { + v, ok := epochEndVals[k] + if !ok { + return num.UintZero() + } + return v + }).Times(len(epochEndVals) * 2) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer1)).Return(num.UintFromUint64(10)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer2)).Return(num.UintFromUint64(20)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer3)).Return(num.UintFromUint64(30)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer4)).Return(num.UintFromUint64(40)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee1)).Return(num.UintFromUint64(50)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee2)).Return(num.UintFromUint64(60)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee3)).Return(num.UintFromUint64(70)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee4)).Return(num.UintFromUint64(80)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee5)).Return(num.UintFromUint64(90)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee6)).Return(num.UintFromUint64(100)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee7)).Return(num.UintFromUint64(110)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee8)).Return(num.UintFromUint64(120)).Times(1) + // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referee9)).Return(num.UintFromUint64(130)).Times(1) expectReferralProgramStartedEvent(t, te1) lastEpochStartTime := program1.EndOfProgramTimestamp.Add(-2 * time.Hour) diff --git a/core/volumediscount/engine.go b/core/volumediscount/engine.go index a1f7ca8877c..a8cfc914576 100644 --- a/core/volumediscount/engine.go +++ b/core/volumediscount/engine.go @@ -64,7 +64,16 @@ func New(broker Broker, marketActivityTracker MarketActivityTracker) *Engine { func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { switch ep.Action { case vegapb.EpochAction_EPOCH_ACTION_START: + // whatever current program is + pp := e.currentProgram e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) + // has the program changed, and is the new current program active? + if pp != nil && pp != e.currentProgram && !e.programHasEnded { + // calculate volume for the window of the new program + e.calculatePartiesVolumeForWindow(int(e.currentProgram.WindowLength)) + // update the factors + e.computeFactorsByParty(ctx, ep.Seq) + } case vegapb.EpochAction_EPOCH_ACTION_END: e.updateNotionalVolumeForEpoch() if !e.programHasEnded { diff --git a/core/volumediscount/engine_test.go b/core/volumediscount/engine_test.go index 660e94eecfe..c72b5994911 100644 --- a/core/volumediscount/engine_test.go +++ b/core/volumediscount/engine_test.go @@ -153,6 +153,8 @@ func TestVolumeDiscountProgramLifecycle(t *testing.T) { e := evt.(*events.VolumeDiscountProgramUpdated) require.Equal(t, p2.IntoProto(), e.GetVolumeDiscountProgramUpdated().Program) }).Times(1) + // expect the stats updated event + expectStatsUpdated(t, broker) engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 1)}) // // expire the program diff --git a/core/volumerebate/engine.go b/core/volumerebate/engine.go index 01bc2332fa0..f9de22461a6 100644 --- a/core/volumerebate/engine.go +++ b/core/volumerebate/engine.go @@ -65,7 +65,13 @@ func New(broker Broker, marketActivityTracker MarketActivityTracker) *Engine { func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { switch ep.Action { case vegapb.EpochAction_EPOCH_ACTION_START: + pp := e.currentProgram e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) + if pp != nil && pp != e.currentProgram && !e.programHasEnded { + // update state based on the new program window length + e.updateState() + e.computeFactorsByParty(ctx, ep.Seq) + } case vegapb.EpochAction_EPOCH_ACTION_END: e.updateState() if !e.programHasEnded { diff --git a/core/volumerebate/engine_test.go b/core/volumerebate/engine_test.go index 1a28d643ae1..cc6c51ecc33 100644 --- a/core/volumerebate/engine_test.go +++ b/core/volumerebate/engine_test.go @@ -58,6 +58,7 @@ func TestVolumeRebateProgramLifecycle(t *testing.T) { broker := mocks.NewMockBroker(ctrl) marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) engine := volumerebate.NewSnapshottedEngine(broker, marketActivityTracker) + marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(1) // test snapshot with empty engine hashEmpty, _, err := engine.GetState(key) @@ -83,7 +84,10 @@ func TestVolumeRebateProgramLifecycle(t *testing.T) { // expect an event for the started program broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { - e := evt.(*events.VolumeRebateProgramStarted) + e, ok := evt.(*events.VolumeRebateProgramStarted) + if !ok { + return + } require.Equal(t, p1.IntoProto(), e.GetVolumeRebateProgramStarted().Program) }).Times(1) @@ -118,14 +122,20 @@ func TestVolumeRebateProgramLifecycle(t *testing.T) { // // expect a program updated event broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { - e := evt.(*events.VolumeRebateProgramUpdated) + e, ok := evt.(*events.VolumeRebateProgramUpdated) + if !ok { + return + } require.Equal(t, p2.IntoProto(), e.GetVolumeRebateProgramUpdated().Program) - }).Times(1) + }).Times(2) engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 1)}) // // expire the program broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { - e := evt.(*events.VolumeRebateProgramEnded) + e, ok := evt.(*events.VolumeRebateProgramEnded) + if !ok { + return + } require.Equal(t, p2.Version, e.GetVolumeRebateProgramEnded().Version) }).Times(1) engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 2)}) From 64c91f839125f772e154fc17d0e53bb31a94200f Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Wed, 18 Sep 2024 16:59:43 +0100 Subject: [PATCH 2/7] fix: update tests Signed-off-by: Elias Van Ootegem --- .../features/referrals/0083-RFPR-running_volume.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/integration/features/referrals/0083-RFPR-running_volume.feature b/core/integration/features/referrals/0083-RFPR-running_volume.feature index 0951b9363a8..08b72c93de3 100644 --- a/core/integration/features/referrals/0083-RFPR-running_volume.feature +++ b/core/integration/features/referrals/0083-RFPR-running_volume.feature @@ -358,6 +358,6 @@ Feature: Calculating referral set running volumes # Check running volume correctly calculated for a variety of window lengths Examples: | window length | running volume 1 | running volume 2 | running volume 3 | running volume 4 | - | 1 | 10000 | 10000 | 10000 | 10000 | - | 2 | 10000 | 20000 | 20000 | 20000 | - | 3 | 10000 | 20000 | 30000 | 30000 | + | 1 | 10000 | 20000 | 10000 | 10000 | + | 2 | 10000 | 30000 | 30000 | 20000 | + | 3 | 10000 | 30000 | 40000 | 40000 | From d44762628f04056d773b37d13949a6c094bf5090 Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 20 Sep 2024 15:15:44 +0100 Subject: [PATCH 3/7] feat: add tests for rebate program enactment --- .../features/verified/enactment_HVMR.feature | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 core/integration/features/verified/enactment_HVMR.feature diff --git a/core/integration/features/verified/enactment_HVMR.feature b/core/integration/features/verified/enactment_HVMR.feature new file mode 100644 index 00000000000..43b09b98579 --- /dev/null +++ b/core/integration/features/verified/enactment_HVMR.feature @@ -0,0 +1,189 @@ +Feature: Volume rebate program - program enactment + + Volume rebate program rewards parties who comprise above a specified + fraction of the maker volume on the network in a window with an + extra rebate factor. + + Tests check on program enactment party benefit factors are updated + correctly and applied in the next epoch. + + Background: + + # Initialise the network and register the assets + Given the average block duration is "1" + And the following network parameters are set: + | name | value | + | market.fee.factors.makerFee | 0.01 | + | market.fee.factors.infrastructureFee | 0.01 | + | market.fee.factors.treasuryFee | 0.1 | + | market.fee.factors.buybackFee | 0.1 | + | network.markPriceUpdateMaximumFrequency | 0s | + | validators.epoch.length | 20s | + | market.auction.minimumDuration | 1 | + + And the following assets are registered: + | id | decimal places | quantum | + | USD-0-1 | 0 | 1 | + | MXN-0-1 | 0 | 1 | + + # Initialise the parties and deposit assets + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | aux1 | USD-0-1 | 1000000 | + | aux2 | USD-0-1 | 1000000 | + | aux1 | MXN-0-1 | 10000000 | + | aux2 | MXN-0-1 | 10000000 | + + # Setup the markets + And the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | + | BTC/USD-0-1 | USD | USD-0-1 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | + | BTC/MXN-0-1 | VND | MXN-0-1 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | + And the spot markets: + | id | name | base asset | quote asset | risk model | auction duration | fees | price monitoring | decimal places | position decimal places | sla params | + | MXN-0-1/USD-0-1 | MXN/USD | MXN-0-1 | USD-0-1 | default-log-normal-risk-model | 1 | default-none | default-none | 0 | 0 | default-basic | + | USD-0-1/MXN-0-1 | MXN/USD | USD-0-1 | MXN-0-1 | default-log-normal-risk-model | 1 | default-none | default-none | 0 | 0 | default-basic | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | BTC/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | BTC/USD-0-1 | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | BTC/MXN-0-1 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | BTC/MXN-0-1 | sell | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | MXN-0-1/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | MXN-0-1/USD-0-1 | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | USD-0-1/MXN-0-1 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | USD-0-1/MXN-0-1 | sell | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "2" blocks + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "BTC/USD-0-1" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "MXN-0-1/USD-0-1" + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | party1 | USD-0-1 | 1000000 | + | party2 | USD-0-1 | 1000000 | + | party1 | MXN-0-1 | 10000000 | + | party2 | MXN-0-1 | 10000000 | + + + Scenario: No program currently active, new program enacted, rebate factors applied from the start of the next epoch. + + # First generate some maker volume, so in the next epoch after the program is created party1 will qualify for the rebate factor + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | party1 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | aux1 | | sell | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | seller maker fee | + | party1 | aux1 | 1 | 50000 | sell | 0 | 500 | + + # Enact the new rebate program + Given the volume rebate program tiers named "vrt": + | fraction | rebate | + | 0.0001 | 0.001 | + And the volume rebate program: + | id | tiers | closing timestamp | window length | + | id | vrt | 0 | 1 | + + # Move ahead to the first epoch after enactment, check party1 is receiving rebates proportional to the rebate factor + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | party1 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | aux1 | | sell | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer high volume maker fee | seller high volume maker fee | + | party1 | aux1 | 1 | 50000 | sell | 0 | 50 | + And the following transfers should happen: + | from | to | from account | to account | market id | amount | asset | type | + | | party1 | ACCOUNT_TYPE_FEES_MAKER | ACCOUNT_TYPE_GENERAL | | 50 | USD-0-1 | TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_RECEIVE | + + Examples: + # Check the above scenario for a derivative and spot market + | market | + | BTC/USD-0-1 | + | MXN-0-1/USD-0-1 | + + + Scenario: Program currently active, program update enacted, rebate factors applied from the start of the next epoch. + + # Enact the original referral program and move to the first epoch after enactment + Given the volume rebate program tiers named "vrt": + | fraction | rebate | + | 0.0001 | 0.001 | + And the volume rebate program: + | id | tiers | closing timestamp | window length | + | id | vrt | 0 | 1 | + And the network moves ahead "1" epochs + + # First generate some maker volume, so in the next epoch party1 will qualify for the rebate factor + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | party1 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | aux1 | | sell | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | seller maker fee | + | party1 | aux1 | 1 | 50000 | sell | 0 | 500 | + + # Move ahead an epoch so factors updated, check party1 is receiving rebates proportional to the rebate factor + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | party1 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | aux1 | | sell | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer high volume maker fee | seller high volume maker fee | + | party1 | aux1 | 1 | 50000 | sell | 0 | 50 | + And the following transfers should happen: + | from | to | from account | to account | market id | amount | asset | type | + | | party1 | ACCOUNT_TYPE_FEES_MAKER | ACCOUNT_TYPE_GENERAL | | 50 | USD-0-1 | TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_RECEIVE | + + # Enact an update to the rebate program - doubling the rebates + Given the volume rebate program tiers named "vrt": + | fraction | rebate | + | 0.0001 | 0.002 | + And the volume rebate program: + | id | tiers | closing timestamp | window length | + | id | vrt | 0 | 1 | + + # Before moving to the next epoch, check party1 is still receiving rebates proportional to the original rebate factor + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | party1 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | aux1 | | sell | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer high volume maker fee | seller high volume maker fee | + | party1 | aux1 | 1 | 50000 | sell | 0 | 50 | + And the following transfers should happen: + | from | to | from account | to account | market id | amount | asset | type | + | | party1 | ACCOUNT_TYPE_FEES_MAKER | ACCOUNT_TYPE_GENERAL | | 50 | USD-0-1 | TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_RECEIVE | + + # Move to the first epoch after program update, check party1 is now receiving rebates proportional to the updated rebate factor + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | party1 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | aux1 | | sell | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer high volume maker fee | seller high volume maker fee | + | party1 | aux1 | 1 | 50000 | sell | 0 | 100 | + And the following transfers should happen: + | from | to | from account | to account | market id | amount | asset | type | + | | party1 | ACCOUNT_TYPE_FEES_MAKER | ACCOUNT_TYPE_GENERAL | | 100 | USD-0-1 | TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_RECEIVE | + + Examples: + # Check the above scenario for a derivative and spot market + | market | + | BTC/USD-0-1 | + | MXN-0-1/USD-0-1 | \ No newline at end of file From fe7d1e733a40e647a06063cd18842734898c49fb Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 20 Sep 2024 15:15:52 +0100 Subject: [PATCH 4/7] feat: add tests for discount program enactment --- .../features/verified/enactment_VDPR.feature | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 core/integration/features/verified/enactment_VDPR.feature diff --git a/core/integration/features/verified/enactment_VDPR.feature b/core/integration/features/verified/enactment_VDPR.feature new file mode 100644 index 00000000000..fe7a45f7c85 --- /dev/null +++ b/core/integration/features/verified/enactment_VDPR.feature @@ -0,0 +1,177 @@ +Feature: Volume discount program - program enactment + + Volume discount program rewards parties who comprise above a specified + taker volume with a discount on their fees. + + Tests check on program enactment party benefit factors are updated + correctly and applied in the next epoch. + + Background: + + # Initialise the network and register the assets + Given the average block duration is "1" + And the following network parameters are set: + | name | value | + | market.fee.factors.makerFee | 0.01 | + | market.fee.factors.infrastructureFee | 0.01 | + | market.fee.factors.treasuryFee | 0.1 | + | market.fee.factors.buybackFee | 0.1 | + | network.markPriceUpdateMaximumFrequency | 0s | + | validators.epoch.length | 20s | + | market.auction.minimumDuration | 1 | + + And the following assets are registered: + | id | decimal places | quantum | + | USD-0-1 | 0 | 1 | + | MXN-0-1 | 0 | 1 | + + # Initialise the parties and deposit assets + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | aux1 | USD-0-1 | 1000000 | + | aux2 | USD-0-1 | 1000000 | + | aux1 | MXN-0-1 | 10000000 | + | aux2 | MXN-0-1 | 10000000 | + + # Setup the markets + And the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | + | BTC/USD-0-1 | USD | USD-0-1 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | + | BTC/MXN-0-1 | VND | MXN-0-1 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | + And the spot markets: + | id | name | base asset | quote asset | risk model | auction duration | fees | price monitoring | decimal places | position decimal places | sla params | + | MXN-0-1/USD-0-1 | MXN/USD | MXN-0-1 | USD-0-1 | default-log-normal-risk-model | 1 | default-none | default-none | 0 | 0 | default-basic | + | USD-0-1/MXN-0-1 | MXN/USD | USD-0-1 | MXN-0-1 | default-log-normal-risk-model | 1 | default-none | default-none | 0 | 0 | default-basic | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | BTC/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | BTC/USD-0-1 | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | BTC/MXN-0-1 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | BTC/MXN-0-1 | sell | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | MXN-0-1/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | MXN-0-1/USD-0-1 | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | USD-0-1/MXN-0-1 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | USD-0-1/MXN-0-1 | sell | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "2" blocks + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "BTC/USD-0-1" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "MXN-0-1/USD-0-1" + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | party1 | USD-0-1 | 1000000 | + | party2 | USD-0-1 | 1000000 | + | party1 | MXN-0-1 | 10000000 | + | party2 | MXN-0-1 | 10000000 | + + + Scenario: No program currently active, new program enacted, discount factors applied from the start of the next epoch. + + # First generate some taker volume, so in the next epoch after the program is created party1 will qualify for discount factors + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | + | party1 | aux1 | 1 | 50000 | buy | 500 | 500 | + + # Enact the new volume discount program + Given the volume discount program tiers named "vdt": + | volume | infra factor | liquidity factor | maker factor | + | 1 | 0.1 | 0.1 | 0.1 | + And the volume discount program: + | id | tiers | closing timestamp | window length | + | id1 | vdt | 0 | 1 | + + # Move ahead to the first epoch after enactment, check party1 is receiving discounts proportional to the discount factors + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee volume discount | buyer infrastructure fee volume discount | + | party1 | aux1 | 1 | 50000 | buy | 450 | 450 | 50 | 50 | + + Examples: + # Check the above scenario for a derivative and spot market + | market | + | BTC/USD-0-1 | + | MXN-0-1/USD-0-1 | + + + Scenario: Program currently active, program update enacted, discount factors applied from the start of the next epoch. + + # Enact the original referral program and move to the first epoch after enactment + Given the volume discount program tiers named "vdt": + | volume | infra factor | liquidity factor | maker factor | + | 1 | 0.1 | 0.1 | 0.1 | + And the volume discount program: + | id | tiers | closing timestamp | window length | + | id1 | vdt | 0 | 1 | + And the network moves ahead "1" epochs + + # First generate some taker volume, so in the next epoch party1 will qualify for the discount factors + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | + | party1 | aux1 | 1 | 50000 | buy | 500 | 500 | + + # Move ahead an epoch so factors updated, check party1 is receiving discounts proportional to the discount factors + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee volume discount | buyer infrastructure fee volume discount | + | party1 | aux1 | 1 | 50000 | buy | 450 | 450 | 50 | 50 | + + # Enact an update to the discount program - doubling the discounts + Given the volume discount program tiers named "vdt": + | volume | infra factor | liquidity factor | maker factor | + | 1000 | 0.2 | 0.2 | 0.2 | + And the volume discount program: + | id | tiers | closing timestamp | window length | + | id1 | vdt | 0 | 1 | + + # Before moving to the next epoch, check party1 is still receiving discounts proportional to the original discount factor + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee volume discount | buyer infrastructure fee volume discount | + | party1 | aux1 | 1 | 50000 | buy | 450 | 450 | 50 | 50 | + + # # Move to the first epoch after program update, check party1 is now receiving discounts proportional to the updated discount factor + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then debug trades + And the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee volume discount | buyer infrastructure fee volume discount | + | party1 | aux1 | 1 | 50000 | buy | 400 | 400 | 100 | 100 | + + Examples: + # Check the above scenario for a derivative and spot market + | market | + | BTC/USD-0-1 | + | MXN-0-1/USD-0-1 | \ No newline at end of file From 2afc66d5ab4c865cfcab3b66d5717199f11287b5 Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 20 Sep 2024 15:16:14 +0100 Subject: [PATCH 5/7] feat: add tests for referral program enactment --- .../features/verified/enactment_RFPR.feature | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 core/integration/features/verified/enactment_RFPR.feature diff --git a/core/integration/features/verified/enactment_RFPR.feature b/core/integration/features/verified/enactment_RFPR.feature new file mode 100644 index 00000000000..f9def99078e --- /dev/null +++ b/core/integration/features/verified/enactment_RFPR.feature @@ -0,0 +1,192 @@ +Feature: Referral program - program enactment + + Referral program rewards sets who comprise above a specified + taker volume with a discount on all of their members fees. + + Tests check on program enactment party benefit factors are updated + correctly and applied in the next epoch. + + Background: + + # Initialise the network and register the assets + Given the average block duration is "1" + And the following network parameters are set: + | name | value | + | market.fee.factors.makerFee | 0.01 | + | market.fee.factors.infrastructureFee | 0.01 | + | market.fee.factors.treasuryFee | 0.1 | + | market.fee.factors.buybackFee | 0.1 | + | network.markPriceUpdateMaximumFrequency | 0s | + | validators.epoch.length | 20s | + | market.auction.minimumDuration | 1 | + | referralProgram.maxPartyNotionalVolumeByQuantumPerEpoch | 1000000000 | + + And the following assets are registered: + | id | decimal places | quantum | + | USD-0-1 | 0 | 1 | + | MXN-0-1 | 0 | 1 | + + # Initialise the parties and deposit assets + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | aux1 | USD-0-1 | 1000000 | + | aux2 | USD-0-1 | 1000000 | + | aux1 | MXN-0-1 | 10000000 | + | aux2 | MXN-0-1 | 10000000 | + + # Setup the markets + And the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | + | BTC/USD-0-1 | USD | USD-0-1 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | + | BTC/MXN-0-1 | VND | MXN-0-1 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | + And the spot markets: + | id | name | base asset | quote asset | risk model | auction duration | fees | price monitoring | decimal places | position decimal places | sla params | + | MXN-0-1/USD-0-1 | MXN/USD | MXN-0-1 | USD-0-1 | default-log-normal-risk-model | 1 | default-none | default-none | 0 | 0 | default-basic | + | USD-0-1/MXN-0-1 | MXN/USD | USD-0-1 | MXN-0-1 | default-log-normal-risk-model | 1 | default-none | default-none | 0 | 0 | default-basic | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | BTC/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | BTC/USD-0-1 | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | BTC/MXN-0-1 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | BTC/MXN-0-1 | sell | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | MXN-0-1/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | MXN-0-1/USD-0-1 | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | USD-0-1/MXN-0-1 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | USD-0-1/MXN-0-1 | sell | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "2" blocks + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "BTC/USD-0-1" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "MXN-0-1/USD-0-1" + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | referrer | USD-0-1 | 10000000 | + | referrer | USD-0-1 | 10000000 | + | party1 | USD-0-1 | 10000000 | + | party1 | MXN-0-1 | 10000000 | + And the parties create the following referral codes: + | party | code | is_team | team | + | referrer | referral-code-1 | true | team1 | + And the parties apply the following referral codes: + | party | code | is_team | team | + | party1 | referral-code-1 | true | team1 | + + + Scenario: No program currently active, new program enacted, benefit factors applied from the start of the next epoch. + + # First generate some taker volume, so in the next epoch after the program is created party1 will qualify for benefit factors + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | + | party1 | aux1 | 1 | 50000 | buy | 500 | 500 | + + # Enact the new referral program + Given the referral benefit tiers "rbt": + | minimum running notional taker volume | minimum epochs | referral reward infra factor | referral reward maker factor | referral reward liquidity factor | referral discount infra factor | referral discount maker factor | referral discount liquidity factor | + | 1 | 0 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | + And the referral staking tiers "rst": + | minimum staked tokens | referral reward multiplier | + | 1 | 1 | + And the referral program: + | end of program | window length | benefit tiers | staking tiers | + | 2023-12-12T12:12:12Z | 10 | rbt | rst | + + # Move ahead to the first epoch after enactment, check party1 is receiving discounts proportional to the benefit factors + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee referrer discount | buyer infrastructure fee referrer discount | + | party1 | aux1 | 1 | 50000 | buy | 450 | 450 | 50 | 50 | + + Examples: + # Check the above scenario for a derivative and spot market + | market | + | BTC/USD-0-1 | + | MXN-0-1/USD-0-1 | + + + Scenario: Program currently active, program update enacted, benefit factors applied from the start of the next epoch. + + # Enact the original referral program and move to the first epoch after enactment + Given the referral benefit tiers "rbt": + | minimum running notional taker volume | minimum epochs | referral reward infra factor | referral reward maker factor | referral reward liquidity factor | referral discount infra factor | referral discount maker factor | referral discount liquidity factor | + | 2000 | 1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | + And the referral staking tiers "rst": + | minimum staked tokens | referral reward multiplier | + | 1 | 1 | + And the referral program: + | end of program | window length | benefit tiers | staking tiers | + | 2023-12-12T12:12:12Z | 7 | rbt | rst | + And the network moves ahead "1" epochs + + # First generate some taker volume, so in the next epoch party1 will qualify for the benefit factors + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | + | party1 | aux1 | 1 | 50000 | buy | 500 | 500 | + + # Move ahead an epoch so factors updated, check party1 is receiving discounts proportional to the benefit factors + Given the network moves ahead "2" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee referrer discount | buyer infrastructure fee referrer discount | + | party1 | aux1 | 1 | 50000 | buy | 450 | 450 | 50 | 50 | + + # Enact an update to the referral program - doubling the discounts + Given the referral benefit tiers "rbt": + | minimum running notional taker volume | minimum epochs | referral reward infra factor | referral reward maker factor | referral reward liquidity factor | referral discount infra factor | referral discount maker factor | referral discount liquidity factor | + | 1 | 0 | 0.2 | 0.2 | 0.2 | 0.2 | 0.2 | 0.2 | + And the referral staking tiers "rst": + | minimum staked tokens | referral reward multiplier | + | 1 | 1 | + And the referral program: + | end of program | window length | benefit tiers | staking tiers | + | 2023-12-12T12:12:12Z | 1 | rbt | rst | + + # Before moving to the next epoch, check party1 is still receiving discounts proportional to the original benefit factor + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee referrer discount | buyer infrastructure fee referrer discount | + | party1 | aux1 | 1 | 50000 | buy | 450 | 450 | 50 | 50 | + + # Move to the first epoch after program update, check party1 is now receiving discounts proportional to the updated benefit factor + Given the network moves ahead "1" epochs + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | | sell | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | | + | party1 | | buy | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | | + When the network moves ahead "1" blocks + And the following trades should be executed: + | buyer | seller | size | price | aggressor side | buyer maker fee | buyer infrastructure fee | buyer maker fee referrer discount | buyer infrastructure fee referrer discount | + | party1 | aux1 | 1 | 50000 | buy | 400 | 400 | 100 | 100 | + + Examples: + # Check the above scenario for a derivative and spot market + | market | + | BTC/USD-0-1 | + | MXN-0-1/USD-0-1 | \ No newline at end of file From fbbe990f1dba3e796e6ebee62d3156c40ddda7ed Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Fri, 20 Sep 2024 16:37:48 +0100 Subject: [PATCH 6/7] fix: calculate stats when no previous program was active, update unit tests Signed-off-by: Elias Van Ootegem --- .../features/verified/enactment_HVMR.feature | 2 +- .../features/verified/enactment_RFPR.feature | 2 +- .../features/verified/enactment_VDPR.feature | 2 +- core/referral/engine.go | 3 +- core/referral/snapshot_test.go | 2 +- core/volumediscount/engine.go | 4 +- core/volumediscount/engine_test.go | 10 +++- core/volumediscount/helpers_for_test.go | 26 +++++++-- core/volumerebate/engine.go | 3 +- core/volumerebate/engine_test.go | 14 +++-- core/volumerebate/helpers_for_test.go | 57 +++++++++++++++---- 11 files changed, 91 insertions(+), 34 deletions(-) diff --git a/core/integration/features/verified/enactment_HVMR.feature b/core/integration/features/verified/enactment_HVMR.feature index 43b09b98579..3622f3ce7bc 100644 --- a/core/integration/features/verified/enactment_HVMR.feature +++ b/core/integration/features/verified/enactment_HVMR.feature @@ -186,4 +186,4 @@ Feature: Volume rebate program - program enactment # Check the above scenario for a derivative and spot market | market | | BTC/USD-0-1 | - | MXN-0-1/USD-0-1 | \ No newline at end of file + | MXN-0-1/USD-0-1 | diff --git a/core/integration/features/verified/enactment_RFPR.feature b/core/integration/features/verified/enactment_RFPR.feature index f9def99078e..59b4e2165fc 100644 --- a/core/integration/features/verified/enactment_RFPR.feature +++ b/core/integration/features/verified/enactment_RFPR.feature @@ -189,4 +189,4 @@ Feature: Referral program - program enactment # Check the above scenario for a derivative and spot market | market | | BTC/USD-0-1 | - | MXN-0-1/USD-0-1 | \ No newline at end of file + | MXN-0-1/USD-0-1 | diff --git a/core/integration/features/verified/enactment_VDPR.feature b/core/integration/features/verified/enactment_VDPR.feature index fe7a45f7c85..42c473d171e 100644 --- a/core/integration/features/verified/enactment_VDPR.feature +++ b/core/integration/features/verified/enactment_VDPR.feature @@ -174,4 +174,4 @@ Feature: Volume discount program - program enactment # Check the above scenario for a derivative and spot market | market | | BTC/USD-0-1 | - | MXN-0-1/USD-0-1 | \ No newline at end of file + | MXN-0-1/USD-0-1 | diff --git a/core/referral/engine.go b/core/referral/engine.go index 7b9bcba4f6a..d4949ea013d 100644 --- a/core/referral/engine.go +++ b/core/referral/engine.go @@ -342,7 +342,8 @@ func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { pp := e.currentProgram e.currentEpoch = ep.Seq e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) - if pp != nil && pp != e.currentProgram && !e.programHasEnded { + // we have an active program, and it's new (pp could be nil, or a pointer to the program before it was updated) + if !e.programHasEnded && pp != e.currentProgram { e.computeReferralSetsStats(ctx, ep, false) } case vegapb.EpochAction_EPOCH_ACTION_END: diff --git a/core/referral/snapshot_test.go b/core/referral/snapshot_test.go index 140a2618603..8e37562fcc1 100644 --- a/core/referral/snapshot_test.go +++ b/core/referral/snapshot_test.go @@ -112,7 +112,7 @@ func TestTakingAndRestoringSnapshotSucceeds(t *testing.T) { return num.UintZero() } return v - }).Times(len(epochEndVals) * 2) + }).Times(len(epochEndVals) * 3) // once for creation, once for new epoch, once for update // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer1)).Return(num.UintFromUint64(10)).Times(1) // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer2)).Return(num.UintFromUint64(20)).Times(1) // te1.marketActivityTracker.EXPECT().NotionalTakerVolumeForParty(string(referrer3)).Return(num.UintFromUint64(30)).Times(1) diff --git a/core/volumediscount/engine.go b/core/volumediscount/engine.go index a8cfc914576..09baaca7ad3 100644 --- a/core/volumediscount/engine.go +++ b/core/volumediscount/engine.go @@ -67,8 +67,8 @@ func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { // whatever current program is pp := e.currentProgram e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) - // has the program changed, and is the new current program active? - if pp != nil && pp != e.currentProgram && !e.programHasEnded { + // we have an active program, and it's not the same one after we called applyProgramUpdate -> update factors. + if !e.programHasEnded && pp != e.currentProgram { // calculate volume for the window of the new program e.calculatePartiesVolumeForWindow(int(e.currentProgram.WindowLength)) // update the factors diff --git a/core/volumediscount/engine_test.go b/core/volumediscount/engine_test.go index c72b5994911..f190cebafb5 100644 --- a/core/volumediscount/engine_test.go +++ b/core/volumediscount/engine_test.go @@ -98,10 +98,12 @@ func TestVolumeDiscountProgramLifecycle(t *testing.T) { engine.UpdateProgram(p1) // expect an event for the started program - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramStarted]{}).DoAndReturn(func(evt events.Event) { e := evt.(*events.VolumeDiscountProgramStarted) require.Equal(t, p1.IntoProto(), e.GetVolumeDiscountProgramStarted().Program) }).Times(1) + // we expect the stats to be updated when a new program starts + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountStatsUpdated]{}).Times(1) // activate the program engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now}) @@ -149,7 +151,7 @@ func TestVolumeDiscountProgramLifecycle(t *testing.T) { assertSnapshotMatches(t, key, hashWithNewAndCurrent) // // expect a program updated event - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramUpdated]{}).DoAndReturn(func(evt events.Event) { e := evt.(*events.VolumeDiscountProgramUpdated) require.Equal(t, p2.IntoProto(), e.GetVolumeDiscountProgramUpdated().Program) }).Times(1) @@ -158,7 +160,7 @@ func TestVolumeDiscountProgramLifecycle(t *testing.T) { engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 1)}) // // expire the program - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramEnded]{}).DoAndReturn(func(evt events.Event) { e := evt.(*events.VolumeDiscountProgramEnded) require.Equal(t, p2.Version, e.GetVolumeDiscountProgramEnded().Version) }).Times(1) @@ -213,6 +215,7 @@ func TestDiscountFactor(t *testing.T) { // activate the program currentEpoch := uint64(1) expectProgramStarted(t, broker, p1) + expectStatsUpdated(t, broker) startEpoch(t, engine, currentEpoch, currentTime) // so now we have a program active so at the end of the epoch lets return for some parties some notional @@ -330,6 +333,7 @@ func TestDiscountFactorWithWindow(t *testing.T) { // expect an event for the started program expectProgramStarted(t, broker, p1) + expectStatsUpdated(t, broker) // activate the program currentEpoch := uint64(1) startEpoch(t, engine, currentEpoch, currentTime) diff --git a/core/volumediscount/helpers_for_test.go b/core/volumediscount/helpers_for_test.go index 1a70382ec82..82a96d9edaf 100644 --- a/core/volumediscount/helpers_for_test.go +++ b/core/volumediscount/helpers_for_test.go @@ -17,6 +17,7 @@ package volumediscount_test import ( "context" + "fmt" "testing" "time" @@ -26,10 +27,25 @@ import ( "code.vegaprotocol.io/vega/core/volumediscount/mocks" vegapb "code.vegaprotocol.io/vega/protos/vega" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) +type vdEvents interface { + *events.VolumeDiscountStatsUpdated | *events.VolumeDiscountProgramStarted | *events.VolumeDiscountProgramUpdated | *events.VolumeDiscountProgramEnded +} + +type eventMatcher[T vdEvents] struct{} + +func (_ eventMatcher[T]) Matches(x any) bool { + _, ok := x.(T) + return ok +} + +func (_ eventMatcher[T]) String() string { + var e T + return fmt.Sprintf("matches %T", e) +} + func endEpoch(t *testing.T, engine *volumediscount.SnapshottedEngine, seq uint64, endTime time.Time) { t.Helper() @@ -53,7 +69,7 @@ func startEpoch(t *testing.T, engine *volumediscount.SnapshottedEngine, seq uint func expectProgramEnded(t *testing.T, broker *mocks.MockBroker, p1 *types.VolumeDiscountProgram) { t.Helper() - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramEnded]{}).DoAndReturn(func(evt events.Event) { e := evt.(*events.VolumeDiscountProgramEnded) require.Equal(t, p1.Version, e.GetVolumeDiscountProgramEnded().Version) }).Times(1) @@ -62,7 +78,7 @@ func expectProgramEnded(t *testing.T, broker *mocks.MockBroker, p1 *types.Volume func expectStatsUpdated(t *testing.T, broker *mocks.MockBroker) { t.Helper() - broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountStatsUpdated]{}).Do(func(evt events.Event) { _, ok := evt.(*events.VolumeDiscountStatsUpdated) require.Truef(t, ok, "expecting event of type *events.VolumeDiscountStatsUpdated but got %T", evt) }).Times(1) @@ -71,7 +87,7 @@ func expectStatsUpdated(t *testing.T, broker *mocks.MockBroker) { func expectStatsUpdatedWithUnqualifiedParties(t *testing.T, broker *mocks.MockBroker) { t.Helper() - broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountStatsUpdated]{}).Do(func(evt events.Event) { update, ok := evt.(*events.VolumeDiscountStatsUpdated) require.Truef(t, ok, "expecting event of type *events.VolumeDiscountStatsUpdated but got %T", evt) stats := update.VolumeDiscountStatsUpdated() @@ -90,7 +106,7 @@ func expectStatsUpdatedWithUnqualifiedParties(t *testing.T, broker *mocks.MockBr func expectProgramStarted(t *testing.T, broker *mocks.MockBroker, p1 *types.VolumeDiscountProgram) { t.Helper() - broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramStarted]{}).Do(func(evt events.Event) { e, ok := evt.(*events.VolumeDiscountProgramStarted) require.Truef(t, ok, "expecting event of type *events.VolumeDiscountProgramStarted but got %T", evt) require.Equal(t, p1.IntoProto(), e.GetVolumeDiscountProgramStarted().Program) diff --git a/core/volumerebate/engine.go b/core/volumerebate/engine.go index f9de22461a6..91cda0fb50c 100644 --- a/core/volumerebate/engine.go +++ b/core/volumerebate/engine.go @@ -67,7 +67,8 @@ func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { case vegapb.EpochAction_EPOCH_ACTION_START: pp := e.currentProgram e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) - if pp != nil && pp != e.currentProgram && !e.programHasEnded { + // we have an active program and it changed after the apply update call -> update state and factors. + if !e.programHasEnded && pp != e.currentProgram { // update state based on the new program window length e.updateState() e.computeFactorsByParty(ctx, ep.Seq) diff --git a/core/volumerebate/engine_test.go b/core/volumerebate/engine_test.go index cc6c51ecc33..6807e6af946 100644 --- a/core/volumerebate/engine_test.go +++ b/core/volumerebate/engine_test.go @@ -58,7 +58,7 @@ func TestVolumeRebateProgramLifecycle(t *testing.T) { broker := mocks.NewMockBroker(ctrl) marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) engine := volumerebate.NewSnapshottedEngine(broker, marketActivityTracker) - marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(1) + marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(2) // test snapshot with empty engine hashEmpty, _, err := engine.GetState(key) @@ -83,13 +83,11 @@ func TestVolumeRebateProgramLifecycle(t *testing.T) { engine.UpdateProgram(p1) // expect an event for the started program - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { - e, ok := evt.(*events.VolumeRebateProgramStarted) - if !ok { - return - } + broker.EXPECT().Send(startedEvt).DoAndReturn(func(evt events.Event) { + e := startedEvt.cast(evt) require.Equal(t, p1.IntoProto(), e.GetVolumeRebateProgramStarted().Program) }).Times(1) + broker.EXPECT().Send(statsEvt).Times(1) // activate the program engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now}) @@ -174,6 +172,8 @@ func TestRebateFactor(t *testing.T) { // activate the program currentEpoch := uint64(1) expectProgramStarted(t, broker, p1) + marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(1) + expectStatsUpdated(t, broker) startEpoch(t, engine, currentEpoch, currentTime) // so now we have a program active so at the end of the epoch lets return for some parties some notional @@ -287,6 +287,8 @@ func TestRebateFactorWithWindow(t *testing.T) { // expect an event for the started program expectProgramStarted(t, broker, p1) + marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(1) + expectStatsUpdated(t, broker) // activate the program currentEpoch := uint64(1) startEpoch(t, engine, currentEpoch, currentTime) diff --git a/core/volumerebate/helpers_for_test.go b/core/volumerebate/helpers_for_test.go index d02bffae38f..3d6e3520b2c 100644 --- a/core/volumerebate/helpers_for_test.go +++ b/core/volumerebate/helpers_for_test.go @@ -17,6 +17,7 @@ package volumerebate_test import ( "context" + "fmt" "testing" "time" @@ -26,10 +27,42 @@ import ( "code.vegaprotocol.io/vega/core/volumerebate/mocks" vegapb "code.vegaprotocol.io/vega/protos/vega" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) +// event matchers for relevant events. +var ( + endedEvt = evtMatcher[events.VolumeRebateProgramEnded]{} + startedEvt = evtMatcher[events.VolumeRebateProgramStarted]{} + updatedEvt = evtMatcher[events.VolumeRebateProgramUpdated]{} + statsEvt = evtMatcher[events.VolumeRebateStatsUpdated]{} +) + +type evts interface { + events.VolumeRebateStatsUpdated | events.VolumeRebateProgramStarted | events.VolumeRebateProgramUpdated | events.VolumeRebateProgramEnded +} + +type evtMatcher[T evts] struct{} + +func (_ evtMatcher[T]) String() string { + var e *T + return fmt.Sprintf("matches %T", e) +} + +func (_ evtMatcher[T]) Matches(x any) bool { + _, ok := x.(*T) + return ok +} + +// cast uses the matcher for the type assertions in the callbacks, returns nil if the input is incompatible, using the correct matcher should make that impossible. +func (_ evtMatcher[T]) cast(v any) *T { + e, ok := v.(*T) + if !ok { + return nil + } + return e +} + func endEpoch(t *testing.T, engine *volumerebate.SnapshottedEngine, seq uint64, endTime time.Time) { t.Helper() @@ -53,8 +86,8 @@ func startEpoch(t *testing.T, engine *volumerebate.SnapshottedEngine, seq uint64 func expectProgramEnded(t *testing.T, broker *mocks.MockBroker, p1 *types.VolumeRebateProgram) { t.Helper() - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { - e := evt.(*events.VolumeRebateProgramEnded) + broker.EXPECT().Send(endedEvt).DoAndReturn(func(evt events.Event) { + e := endedEvt.cast(evt) require.Equal(t, p1.Version, e.GetVolumeRebateProgramEnded().Version) }).Times(1) } @@ -62,18 +95,18 @@ func expectProgramEnded(t *testing.T, broker *mocks.MockBroker, p1 *types.Volume func expectStatsUpdated(t *testing.T, broker *mocks.MockBroker) { t.Helper() - broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - _, ok := evt.(*events.VolumeRebateStatsUpdated) - require.Truef(t, ok, "expecting event of type *events.VolumeRebateStatsUpdated but got %T", evt) + broker.EXPECT().Send(statsEvt).Do(func(evt events.Event) { + e := statsEvt.cast(evt) + require.NotNil(t, e, "expecting non-nil event of type %s but got %T (nil)", statsEvt, evt) }).Times(1) } func expectStatsUpdatedWithUnqualifiedParties(t *testing.T, broker *mocks.MockBroker) { t.Helper() - broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - update, ok := evt.(*events.VolumeRebateStatsUpdated) - require.Truef(t, ok, "expecting event of type *events.VolumeRebateStatsUpdated but got %T", evt) + broker.EXPECT().Send(statsEvt).Do(func(evt events.Event) { + update := statsEvt.cast(evt) + require.NotNil(t, update, "expecting event of type %s but got %T (nil)", statsEvt, evt) stats := update.VolumeRebateStatsUpdated() foundUnqualifiedParty := false for _, s := range stats.Stats { @@ -90,9 +123,9 @@ func expectStatsUpdatedWithUnqualifiedParties(t *testing.T, broker *mocks.MockBr func expectProgramStarted(t *testing.T, broker *mocks.MockBroker, p1 *types.VolumeRebateProgram) { t.Helper() - broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.VolumeRebateProgramStarted) - require.Truef(t, ok, "expecting event of type *events.VolumeRebateProgramStarted but got %T", evt) + broker.EXPECT().Send(startedEvt).Do(func(evt events.Event) { + e := startedEvt.cast(evt) + require.NotNil(t, e, "expecting event of type %s but got %T (nil)", startedEvt, evt) require.Equal(t, p1.IntoProto(), e.GetVolumeRebateProgramStarted().Program) }).Times(1) } From a5b81a8d4dea1475eeff354d7c4b92a3edf2562c Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Mon, 23 Sep 2024 10:03:25 +0100 Subject: [PATCH 7/7] fix: make the linter happy, fix a lifecycle unit test Signed-off-by: Elias Van Ootegem --- core/volumerebate/engine_test.go | 17 ++++++----------- core/volumerebate/helpers_for_test.go | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/core/volumerebate/engine_test.go b/core/volumerebate/engine_test.go index 6807e6af946..03ba8eebd07 100644 --- a/core/volumerebate/engine_test.go +++ b/core/volumerebate/engine_test.go @@ -119,21 +119,16 @@ func TestVolumeRebateProgramLifecycle(t *testing.T) { assertSnapshotMatches(t, key, hashWithNewAndCurrent) // // expect a program updated event - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { - e, ok := evt.(*events.VolumeRebateProgramUpdated) - if !ok { - return - } + broker.EXPECT().Send(updatedEvt).DoAndReturn(func(evt events.Event) { + e := evt.(*events.VolumeRebateProgramUpdated) require.Equal(t, p2.IntoProto(), e.GetVolumeRebateProgramUpdated().Program) - }).Times(2) + }).Times(1) + broker.EXPECT().Send(statsEvt).Times(1) engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 1)}) // // expire the program - broker.EXPECT().Send(gomock.Any()).DoAndReturn(func(evt events.Event) { - e, ok := evt.(*events.VolumeRebateProgramEnded) - if !ok { - return - } + broker.EXPECT().Send(endedEvt).DoAndReturn(func(evt events.Event) { + e := evt.(*events.VolumeRebateProgramEnded) require.Equal(t, p2.Version, e.GetVolumeRebateProgramEnded().Version) }).Times(1) engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 2)}) diff --git a/core/volumerebate/helpers_for_test.go b/core/volumerebate/helpers_for_test.go index 3d6e3520b2c..1c0ab53c0d0 100644 --- a/core/volumerebate/helpers_for_test.go +++ b/core/volumerebate/helpers_for_test.go @@ -34,8 +34,8 @@ import ( var ( endedEvt = evtMatcher[events.VolumeRebateProgramEnded]{} startedEvt = evtMatcher[events.VolumeRebateProgramStarted]{} - updatedEvt = evtMatcher[events.VolumeRebateProgramUpdated]{} statsEvt = evtMatcher[events.VolumeRebateStatsUpdated]{} + updatedEvt = evtMatcher[events.VolumeRebateProgramUpdated]{} ) type evts interface {