diff --git a/commands/proposal_submission.go b/commands/proposal_submission.go
index 1e1a7193a1..01dceb7fef 100644
--- a/commands/proposal_submission.go
+++ b/commands/proposal_submission.go
@@ -461,7 +461,7 @@ func checkVolumeRebateProgramChanges(changes *vegapb.VolumeRebateProgramChanges,
func checkVolumeRebateBenefitTier(index int, tier *vegapb.VolumeRebateBenefitTier) Errors {
errs := NewErrors()
- propertyPath := fmt.Sprintf("update_volume_discount_program.changes.benefit_tiers.%d", index)
+ propertyPath := fmt.Sprintf("update_volume_rebate_program.changes.benefit_tiers.%d", index)
if len(tier.MinimumPartyMakerVolumeFraction) == 0 {
errs.AddForProperty(propertyPath+".minimum_party_maker_volume_fraction", ErrIsRequired)
} else {
diff --git a/commands/proposal_submission_update_rebate_program_test.go b/commands/proposal_submission_update_rebate_program_test.go
new file mode 100644
index 0000000000..d009304a60
--- /dev/null
+++ b/commands/proposal_submission_update_rebate_program_test.go
@@ -0,0 +1,173 @@
+// Copyright (C) 2023 Gobalsky Labs Limited
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package commands_test
+
+import (
+ "testing"
+ "time"
+
+ "code.vegaprotocol.io/vega/commands"
+ types "code.vegaprotocol.io/vega/protos/vega"
+ commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestVolumeRebateSubmission(t *testing.T) {
+ t.Run("empty submission", testUpdateRebateProgram)
+ t.Run("0095-HVMR-001: invalid end timestamp", testInvalidEndTime)
+ t.Run("0095-HVMR-003: tier validation", testInvalidTiers)
+ t.Run("0095-HVMR-004: invalid window length", testInvalidWindowLength)
+}
+
+func testUpdateRebateProgram(t *testing.T) {
+ err := checkProposalSubmission(&commandspb.ProposalSubmission{
+ Terms: &types.ProposalTerms{
+ Change: &types.ProposalTerms_UpdateVolumeRebateProgram{},
+ },
+ })
+
+ assert.Contains(t, err.Get("proposal_submission.terms.change.update_volume_rebate_program"), commands.ErrIsRequired)
+ // missing changes, same problem
+ err = checkProposalSubmission(&commandspb.ProposalSubmission{
+ Terms: &types.ProposalTerms{
+ Change: &types.ProposalTerms_UpdateVolumeRebateProgram{
+ UpdateVolumeRebateProgram: &types.UpdateVolumeRebateProgram{},
+ },
+ },
+ })
+
+ assert.Contains(t, err.Get("proposal_submission.terms.change.update_volume_rebate_program.changes"), commands.ErrIsRequired)
+}
+
+// testInvalidEndTime covers 0095-HVMR-001.
+func testInvalidEndTime(t *testing.T) {
+ end := time.Now()
+ enact := end.Add(time.Second)
+ prop := &commandspb.ProposalSubmission{
+ Terms: &types.ProposalTerms{
+ EnactmentTimestamp: enact.Unix(),
+ Change: &types.ProposalTerms_UpdateVolumeRebateProgram{
+ UpdateVolumeRebateProgram: &types.UpdateVolumeRebateProgram{
+ Changes: &types.VolumeRebateProgramChanges{
+ EndOfProgramTimestamp: end.Unix(),
+ },
+ },
+ },
+ },
+ }
+ err := checkProposalSubmission(prop)
+ assert.Contains(t, err.Get("proposal_submission.terms.change.update_volume_rebate_program.changes.end_of_program_timestamp"), commands.ErrMustBeGreaterThanEnactmentTimestamp)
+}
+
+// testInvalidWindowLength covers 0095-HVMR-004.
+func testInvalidWindowLength(t *testing.T) {
+ enact := time.Now()
+ end := enact.Add(time.Second)
+ prop := &commandspb.ProposalSubmission{
+ Terms: &types.ProposalTerms{
+ EnactmentTimestamp: enact.Unix(),
+ Change: &types.ProposalTerms_UpdateVolumeRebateProgram{
+ UpdateVolumeRebateProgram: &types.UpdateVolumeRebateProgram{
+ Changes: &types.VolumeRebateProgramChanges{
+ EndOfProgramTimestamp: end.Unix(),
+ WindowLength: 0, // zero is invalid
+ },
+ },
+ },
+ },
+ }
+ err := checkProposalSubmission(prop)
+ assert.Contains(t, err.Get("proposal_submission.terms.change.update_volume_rebate_program.changes.window_length"), commands.ErrIsRequired)
+ // now too high of a value
+ prop = &commandspb.ProposalSubmission{
+ Terms: &types.ProposalTerms{
+ EnactmentTimestamp: enact.Unix(),
+ Change: &types.ProposalTerms_UpdateVolumeRebateProgram{
+ UpdateVolumeRebateProgram: &types.UpdateVolumeRebateProgram{
+ Changes: &types.VolumeRebateProgramChanges{
+ EndOfProgramTimestamp: end.Unix(),
+ WindowLength: 10000,
+ },
+ },
+ },
+ },
+ }
+ err = checkProposalSubmission(prop)
+ assert.Contains(t, err.Get("proposal_submission.terms.change.update_volume_rebate_program.changes.window_length"), commands.ErrMustBeAtMost200)
+}
+
+// testInvalidTiers covers 0095-HVMR-003.
+func testInvalidTiers(t *testing.T) {
+ errMap := map[string]error{
+ "proposal_submission.terms.change.update_volume_rebate_program.changes.benefit_tiers.0.minimum_party_maker_volume_fraction": commands.ErrIsRequired,
+ "proposal_submission.terms.change.update_volume_rebate_program.changes.benefit_tiers.1.minimum_party_maker_volume_fraction": commands.ErrIsNotValidNumber,
+ "proposal_submission.terms.change.update_volume_rebate_program.changes.benefit_tiers.2.minimum_party_maker_volume_fraction": commands.ErrMustBePositive,
+ "proposal_submission.terms.change.update_volume_rebate_program.changes.benefit_tiers.3.minimum_party_maker_volume_fraction": commands.ErrMustBePositive,
+ "proposal_submission.terms.change.update_volume_rebate_program.changes.benefit_tiers.4.additional_maker_rebate": commands.ErrIsRequired,
+ "proposal_submission.terms.change.update_volume_rebate_program.changes.benefit_tiers.5.additional_maker_rebate": commands.ErrIsNotValidNumber,
+ "proposal_submission.terms.change.update_volume_rebate_program.changes.benefit_tiers.6.additional_maker_rebate": commands.ErrMustBePositiveOrZero,
+ }
+ enact := time.Now()
+ end := enact.Add(time.Second)
+ prop := &commandspb.ProposalSubmission{
+ Terms: &types.ProposalTerms{
+ EnactmentTimestamp: enact.Unix(),
+ Change: &types.ProposalTerms_UpdateVolumeRebateProgram{
+ UpdateVolumeRebateProgram: &types.UpdateVolumeRebateProgram{
+ Changes: &types.VolumeRebateProgramChanges{
+ EndOfProgramTimestamp: end.Unix(),
+ WindowLength: 10,
+ BenefitTiers: []*types.VolumeRebateBenefitTier{
+ {
+ MinimumPartyMakerVolumeFraction: "",
+ AdditionalMakerRebate: "0.1",
+ },
+ {
+ MinimumPartyMakerVolumeFraction: "invalid",
+ AdditionalMakerRebate: "0.1",
+ },
+ {
+ MinimumPartyMakerVolumeFraction: "-1",
+ AdditionalMakerRebate: "0.1",
+ },
+ {
+ MinimumPartyMakerVolumeFraction: "0",
+ AdditionalMakerRebate: "0.1",
+ },
+ {
+ MinimumPartyMakerVolumeFraction: "0.1",
+ AdditionalMakerRebate: "",
+ },
+ {
+ MinimumPartyMakerVolumeFraction: "0.1",
+ AdditionalMakerRebate: "invalid",
+ },
+ {
+ MinimumPartyMakerVolumeFraction: "0.1",
+ AdditionalMakerRebate: "-0.1",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ err := checkProposalSubmission(prop)
+ for g, c := range errMap {
+ assert.Contains(t, err.Get(g), c, err.Error())
+ }
+}
diff --git a/core/governance/engine_update_referral_program_test.go b/core/governance/engine_update_referral_program_test.go
index 4bf5f1fea1..f8516efdb2 100644
--- a/core/governance/engine_update_referral_program_test.go
+++ b/core/governance/engine_update_referral_program_test.go
@@ -108,6 +108,7 @@ func testSubmittingProposalForReferralProgramUpdateSucceeds(t *testing.T) {
require.NotNil(t, toSubmit)
}
+// testSubmittingProposalForReferralProgramUpdateWithTooManyTiersFails covers 0095-HVMR-002.
func testSubmittingProposalForReferralProgramUpdateWithTooManyTiersFails(t *testing.T) {
now := time.Now()
ctx := vgtest.VegaContext(vgrand.RandomStr(5), vgtest.RandomPositiveI64())
@@ -324,6 +325,7 @@ func testSubmittingProposalForReferralProgramUpdateWithTooHighDiscountFactorFail
require.Nil(t, toSubmit)
}
+// testSubmittingProposalForReferralProgramUpdateEndsBeforeEnactsFails covers 0095-HVMR-001.
func testSubmittingProposalForReferralProgramUpdateEndsBeforeEnactsFails(t *testing.T) {
now := time.Now()
ctx := vgtest.VegaContext(vgrand.RandomStr(5), vgtest.RandomPositiveI64())
diff --git a/core/integration/features/verified/finite-rewards.feature b/core/integration/features/verified/finite-rewards.feature
new file mode 100644
index 0000000000..7205098e82
--- /dev/null
+++ b/core/integration/features/verified/finite-rewards.feature
@@ -0,0 +1,118 @@
+Feature: Team Rewards
+
+ Setup a maker fees received team game with a fee cap.
+
+ We want to make it so that one team is allocated rewards and the other team is allocated rewards.
+
+ - Team A should have also paid rewards and not have their rewards capped.
+ - Team B should not have paid rewards and have their rewards capped.
+
+ Question is what happens to the left over rewards.
+
+
+ Background:
+
+ And the average block duration is "1"
+ And the following network parameters are set:
+ | name | value |
+ | referralProgram.minStakedVegaTokens | 0 |
+ | market.fee.factors.makerFee | 0.01 |
+ | network.markPriceUpdateMaximumFrequency | 0s |
+ | validators.epoch.length | 60s |
+
+ # Initialise the markets
+ And the following assets are registered:
+ | id | decimal places | quantum |
+ | USD-0-1 | 0 | 1 |
+ 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 |
+ | ETH/USD-0-1 | ETH | 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 |
+
+ # Initialise the parties
+ Given the parties deposit on asset's general account the following amount:
+ | party | asset | amount |
+ | aux1 | USD-0-1 | 1000000000000 |
+ | aux2 | USD-0-1 | 1000000000000 |
+ | party1 | USD-0-1 | 1000000000000 |
+ | party2 | USD-0-1 | 1000000000000 |
+ | a3c024b4e23230c89884a54a813b1ecb4cb0f827a38641c66eeca466da6b2ddf | USD-0-1 | 1000000000000 |
+
+ # Exit opening auctions
+ When the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | aux1 | ETH/USD-0-1 | buy | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux2 | ETH/USD-0-1 | sell | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ And the opening auction period ends for market "ETH/USD-0-1"
+ And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "ETH/USD-0-1"
+
+ Given the parties create the following referral codes:
+ | party | code | is_team | team |
+ | party1 | referral-code-1 | true | team1 |
+ Given the parties create the following referral codes:
+ | party | code | is_team | team |
+ | party2 | referral-code-1 | true | team2 |
+
+ Scenario: Check a one-off pay out can be done with start epoch = end epoch
+
+ Given the current epoch is "0"
+ When the parties submit the following recurring transfers:
+ | id | from | from_account_type | to | to_account_type | entity_scope | asset | amount | start_epoch | end_epoch | factor | metric | metric_asset | markets | lock_period | window_length | ntop | cap_reward_fee_multiple |
+ | 1 | a3c024b4e23230c89884a54a813b1ecb4cb0f827a38641c66eeca466da6b2ddf | ACCOUNT_TYPE_GENERAL | 0000000000000000000000000000000000000000000000000000000000000000 | ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES | TEAMS | USD-0-1 | 100 | 1 | 2 | 1 | DISPATCH_METRIC_MAKER_FEES_RECEIVED | USD-0-1 | ETH/USD-0-1 | 10 | 1 | 1 | 1 |
+ Then the network moves ahead "1" epochs
+
+ ## pary1
+ And the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | aux1 | ETH/USD-0-1 | sell | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | party1 | ETH/USD-0-1 | buy | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ | party1 | ETH/USD-0-1 | buy | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux1 | ETH/USD-0-1 | sell | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ And the following trades should be executed:
+ | buyer | size | price | seller | aggressor side | buyer maker fee | seller maker fee |
+ | party1 | 10 | 1000 | aux1 | buy | 100 | 0 |
+ | party1 | 10 | 1000 | aux1 | sell | 0 | 100 |
+
+ # party2
+ And the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | aux1 | ETH/USD-0-1 | sell | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | party2 | ETH/USD-0-1 | buy | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ And the following trades should be executed:
+ | buyer | size | price | seller | aggressor side | buyer maker fee |
+ | party2 | 10 | 1000 | aux1 | buy | 100 |
+
+ When the network moves ahead "1" epochs
+ And parties should have the following vesting account balances:
+ | party | asset | balance |
+ | party1 | USD-0-1 | 100 |
+ | party2 | USD-0-1 | 0 |
+
+
+ ## pary1
+ And the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | aux1 | ETH/USD-0-1 | sell | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | party1 | ETH/USD-0-1 | buy | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ | party1 | ETH/USD-0-1 | buy | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux1 | ETH/USD-0-1 | sell | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ And the following trades should be executed:
+ | buyer | size | price | seller | aggressor side | buyer maker fee | seller maker fee |
+ | party1 | 10 | 1000 | aux1 | buy | 100 | 0 |
+ | party1 | 10 | 1000 | aux1 | sell | 0 | 100 |
+
+ # party2
+ And the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | aux1 | ETH/USD-0-1 | sell | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | party2 | ETH/USD-0-1 | buy | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ And the following trades should be executed:
+ | buyer | size | price | seller | aggressor side | buyer maker fee |
+ | party2 | 10 | 1000 | aux1 | buy | 100 |
+
+ When the network moves ahead "1" epochs
+ And parties should have the following vesting account balances:
+ | party | asset | balance |
+ | party1 | USD-0-1 | 200 |
+ | party2 | USD-0-1 | 0 |
+
+ Then debug transfers
diff --git a/core/integration/features/verified/quick-test.feature b/core/integration/features/verified/quick-test.feature
new file mode 100644
index 0000000000..3864697c33
--- /dev/null
+++ b/core/integration/features/verified/quick-test.feature
@@ -0,0 +1,94 @@
+Feature: Quick-test
+
+ Scenario:
+ And the average block duration is "1"
+ And the following network parameters are set:
+ | name | value |
+ | market.fee.factors.makerFee | 0 |
+ | market.fee.factors.infrastructureFee | 0 |
+ | network.markPriceUpdateMaximumFrequency | 10s |
+ | validators.epoch.length | 1000s |
+
+ # Initialise the markets
+ And the following assets are registered:
+ | id | decimal places | quantum |
+ | USD-1-10 | 0 | 1 |
+ And the simple risk model named "simple-risk-model":
+ | long | short | max move up | min move down | probability of trading |
+ | 0.01 | 0.01 | 100 | -100 | 0.2 |
+ And the margin calculator named "margin-calculator":
+ | search factor | initial factor | release factor |
+ | 1 | 1 | 1 |
+ 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 |
+ | ETH/USD-1-10 | ETH | USD-1-10 | simple-risk-model | margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 |
+
+ # Initialise the parties
+ Given the parties deposit on asset's general account the following amount:
+ | party | asset | amount |
+ | aux1 | USD-1-10 | 1000000000000 |
+ | aux2 | USD-1-10 | 1000000000000 |
+ | party1 | USD-1-10 | 1000000000000 |
+ | party2 | USD-1-10 | 1000000000000 |
+ | party3 | USD-1-10 | 1000000000000 |
+ | a3c024b4e23230c89884a54a813b1ecb4cb0f827a38641c66eeca466da6b2ddf | USD-1-10 | 1000000000000 |
+
+
+
+
+ # Exit opening auctions
+ When the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | aux1 | ETH/USD-1-10 | buy | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux2 | ETH/USD-1-10 | sell | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ And the opening auction period ends for market "ETH/USD-1-10"
+ And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "ETH/USD-1-10"
+ When the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | aux1 | ETH/USD-1-10 | sell | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux2 | ETH/USD-1-10 | buy | 1 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ When the network moves ahead "11" blocks
+ Then the parties should have the following account balances:
+ | party | asset | market id | margin | general |
+ | aux1 | USD-1-10 | ETH/USD-1-10 | 0 | 1000000000000 |
+ | aux2 | USD-1-10 | ETH/USD-1-10 | 0 | 1000000000000 |
+
+
+
+
+
+ Given the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | party1 | ETH/USD-1-10 | buy | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | party2 | ETH/USD-1-10 | sell | 1 | 1000 | 1 | TYPE_LIMIT | TIF_GTC |
+ When the network moves ahead "1" blocks
+ Then the parties should have the following account balances:
+ | party | asset | market id | margin | general |
+ | party1 | USD-1-10 | ETH/USD-1-10 | 10 | 999999999990 |
+ | party2 | USD-1-10 | ETH/USD-1-10 | 10 | 999999999990 |
+
+
+ # # X
+ Given the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif |
+ | party1 | ETH/USD-1-10 | sell | 1 | 990 | 0 | TYPE_LIMIT | TIF_GTC |
+ | party3 | ETH/USD-1-10 | buy | 1 | 990 | 1 | TYPE_LIMIT | TIF_GTC |
+ When the network moves ahead "1" blocks
+ Then the parties should have the following account balances:
+ | party | asset | market id | margin | general |
+ | party1 | USD-1-10 | ETH/USD-1-10 | 0 | 1000000000000 |
+ | party2 | USD-1-10 | ETH/USD-1-10 | 10 | 999999999990 |
+ | party3 | USD-1-10 | ETH/USD-1-10 | 10 | 999999999990 |
+
+ When the parties withdraw the following assets:
+ | party | asset | amount | error |
+ | party1 | USD-1-10 | 1000000000000 | |
+
+
+ Then the network moves ahead "100" blocks
+ Then debug transfers
+ Then the parties should have the following account balances:
+ | party | asset | market id | margin | general |
+ | party1 | USD-1-10 | ETH/USD-1-10 | 0 | 0 |
+ | party2 | USD-1-10 | ETH/USD-1-10 | 11 | 999999999989 |
+ | party3 | USD-1-10 | ETH/USD-1-10 | 11 | 999999999989 |
diff --git a/core/integration/features/volume-rebate/0095-HVMR-cap.feature b/core/integration/features/volume-rebate/0095-HVMR-cap.feature
index 014d4fea85..c8cdf9527d 100644
--- a/core/integration/features/volume-rebate/0095-HVMR-cap.feature
+++ b/core/integration/features/volume-rebate/0095-HVMR-cap.feature
@@ -42,7 +42,7 @@ Feature: Volume rebate program - rebate cap
| party2 | USD-0-1 | 1000000 |
- Scenario Outline: Fixed buyback, treasury and rebate factors. Rebate capped correctly where necessary. (0095-HVMR-029)(0095-HVMR-030)(0095-HVMR-31)
+ Scenario Outline: Fixed buyback, treasury and rebate factors. Rebate capped correctly where necessary. (0095-HVMR-029)(0095-HVMR-030)(0095-HVMR-031)
Given the following network parameters are set:
| name | value |
@@ -252,4 +252,64 @@ Feature: Volume rebate program - rebate cap
| 0.003 | 0.000 | 0.002 | 200 | 0 | 0.004 | 300 |
+ Scenario Outline: Fees updated mid epoch, fees not affected untill next epoch (0029-FEES-051)(0029-FEES-052)
+
+ Given the following network parameters are set:
+ | name | value |
+ | market.fee.factors.buybackFee | |
+ | market.fee.factors.treasuryFee | |
+ And the volume rebate program tiers named "vrt":
+ | fraction | rebate |
+ | 0.0001 | |
+ And the volume rebate program:
+ | id | tiers | closing timestamp | window length |
+ | id | vrt | 0 | 1 |
+ And the network moves ahead "1" epochs
+
+ Given the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif | error |
+ | party1 | BTC/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | |
+ | party2 | BTC/USD-0-1 | 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 | party2 | 1 | 50000 | sell | 0 | 500 |
+
+ 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 | BTC/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | |
+ | party2 | BTC/USD-0-1 | 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 | seller treasury fee | seller buyback fee | seller high volume maker fee |
+ | party1 | party2 | 1 | 50000 | sell | | | |
+ Then the following transfers should happen:
+ | from | to | from account | to account | market id | amount | asset | type |
+ | | party1 | ACCOUNT_TYPE_FEES_MAKER | ACCOUNT_TYPE_GENERAL | BTC/USD-0-1 | | USD-0-1 | TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_RECEIVE |
+
+ Then clear trade events
+ Given the following network parameters are set:
+ | name | value |
+ | market.fee.factors.buybackFee | |
+ | market.fee.factors.treasuryFee | |
+ And the network moves ahead "1" blocks
+ When the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif | error |
+ | party1 | BTC/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | |
+ | party2 | BTC/USD-0-1 | sell | 1 | 50000 | 1 | TYPE_LIMIT | TIF_GTC | |
+ When the network moves ahead "1" blocks
+ Then debug trades
+ Then the following trades should be executed:
+ | buyer | seller | size | price | aggressor side | seller treasury fee | seller buyback fee | seller high volume maker fee |
+ | party1 | party2 | 1 | 50000 | sell | | | |
+ Then the following transfers should happen:
+ | from | to | from account | to account | market id | amount | asset | type |
+ | | party1 | ACCOUNT_TYPE_FEES_MAKER | ACCOUNT_TYPE_GENERAL | BTC/USD-0-1 | | USD-0-1 | TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_RECEIVE |
+
+ Examples:
+ | rebate | initial buyback | initial treasury | initial buyback amount | initial treasury amount | initial rebate amount | updated buyback | updated treasury | updated buyback amount | updated treasury amount | updated rebate amount |
+ | 0.003 | 0.004 | 0 | 50 | 0 | 150 | 0.001 | 0 | 0 | 0 | 50 |
+ | 0.003 | 0 | 0.004 | 0 | 50 | 150 | 0 | 0.001 | 0 | 0 | 50 |
+ | 0.003 | 0.002 | 0.002 | 25 | 25 | 150 | 0.001 | 0.001 | 0 | 0 | 100 |
diff --git a/core/integration/features/volume-rebate/0095-HVMR-multiple.feature b/core/integration/features/volume-rebate/0095-HVMR-multiple.feature
new file mode 100644
index 0000000000..763fa804d5
--- /dev/null
+++ b/core/integration/features/volume-rebate/0095-HVMR-multiple.feature
@@ -0,0 +1,179 @@
+Feature: Volume rebate program - two programs overlapping
+ 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-10 | 0 | 10 |
+
+ # Initialise the parties and deposit assets
+ And the parties deposit on asset's general account the following amount:
+ | party | asset | amount |
+ | aux1 | USD-0-1 | 3000000 |
+ | aux2 | USD-0-1 | 3000000 |
+ | aux1 | MXN-0-10 | 30000000 |
+ | aux2 | MXN-0-10 | 30000000 |
+ | party1 | USD-0-1 | 3000000 |
+ | party2 | USD-0-1 | 3000000 |
+ | party1 | MXN-0-10 | 30000000 |
+ | party2 | MXN-0-10 | 30000000 |
+
+ # Setup the markets
+ And the price monitoring named "price-monitoring":
+ | horizon | probability | auction extension |
+ | 3600 | 0.99 | 1 |
+ 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 | price-monitoring | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 |
+ | BTC/MXN-0-10 | VND | MXN-0-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | price-monitoring | 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-10/USD-0-1 | MXN/USD | MXN-0-10 | USD-0-1 | default-log-normal-risk-model | 1 | default-none | price-monitoring | 0 | 0 | default-basic |
+ | USD-0-1/MXN-0-10 | MXN/USD | USD-0-1 | MXN-0-10 | default-log-normal-risk-model | 1 | default-none | price-monitoring | 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-10 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux2 | BTC/MXN-0-10 | 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-10/USD-0-1 | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux2 | MXN-0-10/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-10 | buy | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC |
+ | aux2 | USD-0-1/MXN-0-10 | sell | 1 | 500000 | 0 | TYPE_LIMIT | TIF_GTC |
+ When the network moves ahead "2" blocks
+ Then 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 "BTC/MXN-0-10"
+ And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "MXN-0-10/USD-0-1"
+ And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "MXN-0-10/USD-0-1"
+
+ Scenario Outline: rebate program A is enacted, during its lifetime program B takes over. Program B closes after program A, so after B closes, no programs should be active. (0095-HVMR-005)(0095-HVMR-006)(0095-HMVR-007)(0095-HVMR-008)(0095-HVMR-009)(0095-HVM-010)(0095-HVMR-011).
+ Given the volume rebate program tiers named "A1":
+ | fraction | rebate |
+ | 0.0001 | 0.001 |
+ And the volume rebate program tiers named "B3":
+ | fraction | rebate |
+ | 0.0001 | 0.002 |
+ | 0.0002 | 0.003 |
+ | 0.0003 | 0.004 |
+ And the volume rebate program:
+ | id | tiers | closing timestamp | window length | closing delta |
+ | programA | A1 | 0 | | |
+ And 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 maker fee | seller maker fee |
+ | party1 | aux1 | 1 | 50000 | sell | 0 | 500 |
+
+ Given the network moves ahead epochs
+ When the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif | error |
+ | party2 | | 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 |
+ | party2 | aux1 | 1 | 50000 | sell | 0 | 500 |
+
+ When the network moves ahead "1" epochs
+ When 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 | |
+ | party2 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | |
+ | aux1 | | sell | 2 | 50000 | 2 | 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 | |
+ | party2 | aux1 | 1 | 50000 | sell | 0 | |
+
+ When the network moves ahead "1" epochs
+ Then the volume rebate program:
+ | id | tiers | closing timestamp | window length | closing delta |
+ | programB | B3 | 0 | | |
+
+ When the network moves ahead "1" epochs
+ Then 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 |
+
+ Given the network moves ahead epochs
+ When the parties place the following orders:
+ | party | market id | side | volume | price | resulting trades | type | tif | error |
+ | party2 | | 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 |
+ | party2 | aux1 | 1 | 50000 | sell | 0 | 500 |
+
+ When the network moves ahead "1" epochs
+ Then 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 | |
+ | party2 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | |
+ | aux1 | | sell | 2 | 50000 | 2 | 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 | |
+ | party2 | aux1 | 1 | 50000 | sell | 0 | |
+
+ # Now no programs are active
+ When the network moves ahead "4" epochs
+ Then 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 |
+ | party2 | aux1 | 1 | 50000 | sell | 0 | 500 |
+
+ When the network moves ahead "1" epochs
+ Then 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 | |
+ | party2 | | buy | 1 | 50000 | 0 | TYPE_LIMIT | TIF_GTC | |
+ | aux1 | | sell | 2 | 50000 | 2 | 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 | 0 |
+ | party2 | aux1 | 1 | 50000 | sell | 0 | 0 |
+
+ # Closing delta must be > 1 epoch + epochs betwochs between trades + 3 blocks, 80s == 4 epochs
+ Examples:
+ | market | window length | epochs between trades | party1 rebate A | party2 rebate A | closing delta | party1 rebate B | party2 rebate B |
+ | BTC/USD-0-1 | 2 | "0" | 50 | 50 | 80s | 200 | 200 |
+ | BTC/USD-0-1 | 2 | "1" | 50 | 50 | 80s | 200 | 200 |
+ | MXN-0-10/USD-0-1 | 2 | "0" | 50 | 50 | 80s | 200 | 200 |
+ | MXN-0-10/USD-0-1 | 2 | "1" | 50 | 50 | 80s | 200 | 200 |
+
diff --git a/core/integration/features/volume-rebate/0095-HVMR.feature b/core/integration/features/volume-rebate/0095-HVMR.feature
index f18b9b3c73..ec8cf474ea 100644
--- a/core/integration/features/volume-rebate/0095-HVMR.feature
+++ b/core/integration/features/volume-rebate/0095-HVMR.feature
@@ -222,7 +222,7 @@ Feature: Volume rebate program - contributions from trades
| MXN-0-10/USD-0-1 | 2 | "2" | 0 | 50 |
- Scenario Outline: Derivative and spot markets using assets with different quantums scale maker volume correctly (0095-HVMR-022)(0095-HVMR-023)(0095-HVMR-023)
+ Scenario Outline: Derivative and spot markets using assets with different quantums scale maker volume correctly (0095-HVMR-023)(0095-HVMR-024)(0095-HVMR-025)
Given the volume rebate program tiers named "vrt":
| fraction | rebate |
diff --git a/core/integration/main_test.go b/core/integration/main_test.go
index 379cc72e69..c6e9c680ed 100644
--- a/core/integration/main_test.go
+++ b/core/integration/main_test.go
@@ -819,7 +819,7 @@ func InitializeScenario(s *godog.ScenarioContext) {
})
s.Step(`^the volume rebate program:$`, func(table *godog.Table) error {
- return steps.VolumeRebateProgram(execsetup.volumeRebateProgram, volumeRebateTiers, table)
+ return steps.VolumeRebateProgram(execsetup.volumeRebateProgram, volumeRebateTiers, execsetup.timeService, table)
})
s.Step(`^the party "([^"]*)" has the following rebate factor "([^"]*)"$`, func(party, rebate string) error {
return steps.PartyHasTheFollowingRebate(party, rebate, execsetup.volumeRebateProgram)
diff --git a/core/integration/steps/the_following_trades_happened.go b/core/integration/steps/the_following_trades_happened.go
index 77f8552373..16d98b0d58 100644
--- a/core/integration/steps/the_following_trades_happened.go
+++ b/core/integration/steps/the_following_trades_happened.go
@@ -65,6 +65,8 @@ func TheFollowingTradesShouldBeExecuted(
buyerInfraFeeReferrerDiscount, hasBuyerInfraFeeReferrerDiscount := row.DecimalB("buyer infrastructure fee referrer discount")
buyerMakerFeeReferrerDiscount, hasBuyerMakerFeeReferrerDiscount := row.DecimalB("buyer maker fee referrer discount")
buyerLiqFeeReferrerDiscount, hasBuyerLiqFeeReferrerDiscount := row.DecimalB("buyer liquidity fee referrer discount")
+ buyerBuyBackFee, hasBuyerBuyBackFee := row.DecimalB("buyer buyback fee")
+ buyerTreasuryFee, hasBuyerTreasuryFee := row.DecimalB("buyer treasury fee")
buyerHighVolumeMakerFee, hasBuyerHighVolumeMakerFee := row.DecimalB("buyer high volume maker fee")
sellerFee, hasSellerFee := row.U64B("seller fee")
@@ -77,6 +79,8 @@ func TheFollowingTradesShouldBeExecuted(
sellerInfraFeeReferrerDiscount, hasSellerInfraFeeReferrerDiscount := row.DecimalB("seller infrastructure fee referrer discount")
sellerMakerFeeReferrerDiscount, hasSellerMakerFeeReferrerDiscount := row.DecimalB("seller maker fee referrer discount")
sellerLiqFeeReferrerDiscount, hasSellerLiqFeeReferrerDiscount := row.DecimalB("seller liquidity fee referrer discount")
+ sellerBuyBackFee, hasSellerBuyBackFee := row.DecimalB("seller buyback fee")
+ sellerTreasuryFee, hasSellerTreasuryFee := row.DecimalB("seller treasury fee")
sellerHighVolumeMakerFee, hasSellerHighVolumeMakerFee := row.DecimalB("seller high volume maker fee")
data := broker.GetTrades()
@@ -97,6 +101,8 @@ func TheFollowingTradesShouldBeExecuted(
(!hasBuyerInfraFeeReferrerDiscount || buyerInfraFeeReferrerDiscount.Equal(num.MustDecimalFromString(v.BuyerFee.InfrastructureFeeReferrerDiscount))) &&
(!hasBuyerMakerFeeReferrerDiscount || buyerMakerFeeReferrerDiscount.Equal(num.MustDecimalFromString(v.BuyerFee.MakerFeeReferrerDiscount))) &&
(!hasBuyerLiqFeeReferrerDiscount || buyerLiqFeeReferrerDiscount.Equal(num.MustDecimalFromString(v.BuyerFee.LiquidityFeeReferrerDiscount))) &&
+ (!hasBuyerTreasuryFee || buyerTreasuryFee.Equal(num.MustDecimalFromString(v.BuyerFee.TreasuryFee))) &&
+ (!hasBuyerBuyBackFee || buyerBuyBackFee.Equal(num.MustDecimalFromString(v.BuyerFee.BuyBackFee))) &&
(!hasBuyerHighVolumeMakerFee || buyerHighVolumeMakerFee.Equal(num.MustDecimalFromString(v.BuyerFee.HighVolumeMakerFee))) &&
(!hasSellerFee || sellerFee == feeToU64(v.SellerFee)) &&
(!hasSellerInfraFee || sellerInfraFee == stringToU64(v.SellerFee.InfrastructureFee)) &&
@@ -108,6 +114,8 @@ func TheFollowingTradesShouldBeExecuted(
(!hasSellerInfraFeeReferrerDiscount || sellerInfraFeeReferrerDiscount.Equal(num.MustDecimalFromString(v.SellerFee.InfrastructureFeeReferrerDiscount))) &&
(!hasSellerMakerFeeReferrerDiscount || sellerMakerFeeReferrerDiscount.Equal(num.MustDecimalFromString(v.SellerFee.MakerFeeReferrerDiscount))) &&
(!hasSellerLiqFeeReferrerDiscount || sellerLiqFeeReferrerDiscount.Equal(num.MustDecimalFromString(v.SellerFee.LiquidityFeeReferrerDiscount))) &&
+ (!hasSellerTreasuryFee || sellerTreasuryFee.Equal(num.MustDecimalFromString(v.SellerFee.TreasuryFee))) &&
+ (!hasSellerBuyBackFee || sellerBuyBackFee.Equal(num.MustDecimalFromString(v.SellerFee.BuyBackFee))) &&
(!hasSellerHighVolumeMakerFee || sellerHighVolumeMakerFee.Equal(num.MustDecimalFromString(v.SellerFee.HighVolumeMakerFee))) {
found = true
}
@@ -145,6 +153,8 @@ func parseExecutedTradesTable(table *godog.Table) []RowWrapper {
"buyer infrastructure fee referrer discount",
"buyer liquidity fee referrer discount",
"buyer maker fee referrer discount",
+ "buyer treasury fee",
+ "buyer buyback fee",
"buyer high volume maker fee",
"seller fee",
@@ -157,6 +167,8 @@ func parseExecutedTradesTable(table *godog.Table) []RowWrapper {
"seller infrastructure fee referrer discount",
"seller liquidity fee referrer discount",
"seller maker fee referrer discount",
+ "seller treasury fee",
+ "seller buyback fee",
"seller high volume maker fee",
"is amm",
})
diff --git a/core/integration/steps/volume_rebate_program.go b/core/integration/steps/volume_rebate_program.go
index b6fa9ad67d..34dbfb2d09 100644
--- a/core/integration/steps/volume_rebate_program.go
+++ b/core/integration/steps/volume_rebate_program.go
@@ -19,6 +19,7 @@ import (
"fmt"
"time"
+ "code.vegaprotocol.io/vega/core/integration/stubs"
"code.vegaprotocol.io/vega/core/types"
"code.vegaprotocol.io/vega/core/volumerebate"
"code.vegaprotocol.io/vega/libs/num"
@@ -68,6 +69,7 @@ func (r volumeRebateTiersRow) rebate() num.Decimal {
func VolumeRebateProgram(
vde *volumerebate.Engine,
tiers map[string][]*types.VolumeRebateBenefitTier,
+ ts *stubs.TimeStub,
table *godog.Table,
) error {
rows := parseVolumeRebateTable(table)
@@ -77,11 +79,7 @@ func VolumeRebateProgram(
row := volumeRebateRow{row: r}
vdp.ID = row.id()
vdp.WindowLength = row.windowLength()
- if row.closingTimestamp() == 0 {
- vdp.EndOfProgramTimestamp = time.Time{}
- } else {
- vdp.EndOfProgramTimestamp = time.Unix(row.closingTimestamp(), 0)
- }
+ vdp.EndOfProgramTimestamp = row.closingTS(ts.GetTimeNow())
tierName := row.tiers()
if tier := tiers[tierName]; tier != nil {
vdp.VolumeRebateBenefitTiers = tier
@@ -97,7 +95,9 @@ func parseVolumeRebateTable(table *godog.Table) []RowWrapper {
"tiers",
"closing timestamp",
"window length",
- }, []string{})
+ }, []string{
+ "closing delta",
+ })
}
type volumeRebateRow struct {
@@ -116,6 +116,24 @@ func (r volumeRebateRow) closingTimestamp() int64 {
return r.row.MustI64("closing timestamp")
}
+func (r volumeRebateRow) closingTS(now time.Time) time.Time {
+ if delta, ok := r.closingDelta(); ok {
+ return now.Add(delta)
+ }
+ ts := r.closingTimestamp()
+ if ts == 0 {
+ return time.Time{}
+ }
+ return time.Unix(ts, 0)
+}
+
+func (r volumeRebateRow) closingDelta() (time.Duration, bool) {
+ if r.row.HasColumn("closing delta") {
+ return r.row.MustDurationStr("closing delta"), true
+ }
+ return 0, false
+}
+
func (r volumeRebateRow) windowLength() uint64 {
return r.row.MustU64("window length")
}