fix(trading): use discount stats only from previous epoch (#5411)

Co-authored-by: asiaznik <artur@vegaprotocol.io>
This commit is contained in:
Bartłomiej Głownia 2023-12-01 17:34:05 +01:00 committed by GitHub
parent 70d748fb15
commit 61471228aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 104 additions and 323 deletions

View File

@ -123,7 +123,7 @@ export const Statistics = ({
t.discountFactor === discountFactorValue t.discountFactor === discountFactorValue
); );
const nextBenefitTierValue = currentBenefitTierValue const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1) ? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier : minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
const epochsValue = const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch !isNaN(currentEpoch) && refereeInfo?.atEpoch

View File

@ -16,18 +16,11 @@ query DiscountPrograms {
} }
} }
query Fees( query Fees($partyId: ID!) {
$partyId: ID!
$volumeDiscountEpochs: Int!
$referralDiscountEpochs: Int!
) {
epoch { epoch {
id id
} }
volumeDiscountStats( volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
partyId: $partyId
pagination: { last: $volumeDiscountEpochs }
) {
edges { edges {
node { node {
atEpoch atEpoch
@ -59,10 +52,7 @@ query Fees(
} }
} }
} }
referralSetStats( referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
partyId: $partyId
pagination: { last: $referralDiscountEpochs }
) {
edges { edges {
node { node {
atEpoch atEpoch

View File

@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
export type FeesQueryVariables = Types.Exact<{ export type FeesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID']; partyId: Types.Scalars['ID'];
volumeDiscountEpochs: Types.Scalars['Int'];
referralDiscountEpochs: Types.Scalars['Int'];
}>; }>;
@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>; export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>; export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
export const FeesDocument = gql` export const FeesDocument = gql`
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) { query Fees($partyId: ID!) {
epoch { epoch {
id id
} }
volumeDiscountStats( volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
partyId: $partyId
pagination: {last: $volumeDiscountEpochs}
) {
edges { edges {
node { node {
atEpoch atEpoch
@ -104,7 +99,7 @@ export const FeesDocument = gql`
} }
} }
} }
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) { referralSetStats(partyId: $partyId, pagination: {last: 1}) {
edges { edges {
node { node {
atEpoch atEpoch
@ -129,8 +124,6 @@ export const FeesDocument = gql`
* const { data, loading, error } = useFeesQuery({ * const { data, loading, error } = useFeesQuery({
* variables: { * variables: {
* partyId: // value for 'partyId' * partyId: // value for 'partyId'
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
* }, * },
* }); * });
*/ */

View File

@ -42,19 +42,19 @@ export const FeesContainer = () => {
programData?.currentVolumeDiscountProgram?.windowLength || 1; programData?.currentVolumeDiscountProgram?.windowLength || 1;
const referralDiscountWindowLength = const referralDiscountWindowLength =
programData?.currentReferralProgram?.windowLength || 1; programData?.currentReferralProgram?.windowLength || 1;
const { data: feesData, loading: feesLoading } = useFeesQuery({ const { data: feesData, loading: feesLoading } = useFeesQuery({
variables: { variables: {
partyId: pubKey || '', partyId: pubKey || '',
volumeDiscountEpochs: volumeDiscountWindowLength,
referralDiscountEpochs: referralDiscountWindowLength,
}, },
skip: !pubKey || !programData, skip: !pubKey,
}); });
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } = const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats( useVolumeStats(
feesData?.volumeDiscountStats, previousEpoch,
feesData?.volumeDiscountStats.edges?.[0]?.node,
programData?.currentVolumeDiscountProgram programData?.currentVolumeDiscountProgram
); );
@ -67,12 +67,12 @@ export const FeesContainer = () => {
code, code,
isReferrer, isReferrer,
} = useReferralStats( } = useReferralStats(
feesData?.referralSetStats, previousEpoch,
feesData?.referralSetReferees, feesData?.referralSetStats.edges?.[0]?.node,
feesData?.referralSetReferees.edges?.[0]?.node,
programData?.currentReferralProgram, programData?.currentReferralProgram,
feesData?.epoch, feesData?.referrer.edges?.[0]?.node,
feesData?.referrer, feesData?.referee.edges?.[0]?.node
feesData?.referee
); );
const loading = paramsLoading || feesLoading || programLoading; const loading = paramsLoading || feesLoading || programLoading;
@ -460,7 +460,7 @@ const VolumeTiers = ({
</THead> </THead>
<tbody> <tbody>
{Array.from(tiers).map((tier, i) => { {Array.from(tiers).map((tier, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i; const isUserTier = tierIndex === i;
return ( return (
<Tr key={i}> <Tr key={i}>
@ -513,7 +513,7 @@ const ReferralTiers = ({
</THead> </THead>
<tbody> <tbody>
{Array.from(tiers).map((t, i) => { {Array.from(tiers).map((t, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i; const isUserTier = tierIndex === i;
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume); const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
let unlocksIn = null; let unlocksIn = null;

View File

@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react';
import { useReferralStats } from './use-referral-stats'; import { useReferralStats } from './use-referral-stats';
describe('useReferralStats', () => { describe('useReferralStats', () => {
const setStats = { const stat = {
edges: [ __typename: 'ReferralSetStats' as const,
{ atEpoch: 9,
__typename: 'ReferralSetStatsEdge' as const, discountFactor: '0.01',
node: { referralSetRunningNotionalTakerVolume: '100',
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.2',
referralSetRunningNotionalTakerVolume: '100',
},
},
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '200',
},
},
],
}; };
const sets = { const set = {
edges: [ atEpoch: 4,
{
node: {
atEpoch: 3,
},
},
{
node: {
atEpoch: 4,
},
},
],
};
const epoch = {
id: '10',
}; };
const program = { const program = {
@ -78,102 +47,36 @@ describe('useReferralStats', () => {
}); });
}); });
it('returns formatted data and tiers', () => { it('returns default values if set is not from previous epoch', () => {
const { result } = renderHook(() => const { result } = renderHook(() =>
useReferralStats(setStats, sets, program, epoch) useReferralStats(10, stat, set, program)
); );
// should use stats from latest epoch
const stats = setStats.edges[1].node;
const set = sets.edges[1].node;
expect(result.current).toEqual({ expect(result.current).toEqual({
referralDiscount: Number(stats.discountFactor), referralDiscount: 0,
referralVolumeInWindow: Number( referralVolumeInWindow: 0,
stats.referralSetRunningNotionalTakerVolume referralTierIndex: -1,
),
referralTierIndex: 1,
referralTiers: program.benefitTiers, referralTiers: program.benefitTiers,
epochsInSet: Number(epoch.id) - set.atEpoch, epochsInSet: 0,
code: undefined, code: undefined,
isReferrer: false, isReferrer: false,
}); });
}); });
it.each([ it('returns formatted data and tiers', () => {
{ joinedAt: 2, index: -1 },
{ joinedAt: 3, index: -1 },
{ joinedAt: 4, index: 0 },
{ joinedAt: 5, index: 0 },
{ joinedAt: 6, index: 1 },
{ joinedAt: 7, index: 1 },
{ joinedAt: 8, index: 2 },
{ joinedAt: 9, index: 2 },
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '100000',
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: Number(epoch.id) - obj.joinedAt,
},
},
],
};
const { result } = renderHook(() => const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch) useReferralStats(9, stat, set, program)
); );
expect(result.current.referralTierIndex).toEqual(obj.index); expect(result.current).toEqual({
}); referralDiscount: Number(stat.discountFactor),
referralVolumeInWindow: Number(
it.each([ stat.referralSetRunningNotionalTakerVolume
{ volume: '50', index: -1 }, ),
{ volume: '100', index: 0 }, referralTierIndex: 0,
{ volume: '150', index: 0 }, referralTiers: program.benefitTiers,
{ volume: '200', index: 1 }, epochsInSet: stat.atEpoch - set.atEpoch,
{ volume: '250', index: 1 }, code: undefined,
{ volume: '300', index: 2 }, isReferrer: false,
{ volume: '999', index: 2 }, });
])('volume: $volume should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: obj.volume,
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: 1,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
}); });
}); });

View File

@ -1,20 +1,24 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getReferralBenefitTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees'; import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
import { first } from 'lodash';
export const useReferralStats = ( export const useReferralStats = (
setStats?: FeesQuery['referralSetStats'], previousEpoch?: number,
setReferees?: FeesQuery['referralSetReferees'], referralStats?: NonNullable<
FeesQuery['referralSetStats']['edges']['0']
>['node'],
setReferees?: NonNullable<
FeesQuery['referralSetReferees']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentReferralProgram'], program?: DiscountProgramsQuery['currentReferralProgram'],
epoch?: FeesQuery['epoch'], setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
setIfReferrer?: FeesQuery['referrer'], setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
setIfReferee?: FeesQuery['referee']
) => { ) => {
const referralTiers = program?.benefitTiers || []; const referralTiers = program?.benefitTiers || [];
if (!setStats || !setReferees || !program || !epoch) { if (
!previousEpoch ||
referralStats?.atEpoch !== previousEpoch ||
!program ||
!setReferees
) {
return { return {
referralDiscount: 0, referralDiscount: 0,
referralVolumeInWindow: 0, referralVolumeInWindow: 0,
@ -26,41 +30,22 @@ export const useReferralStats = (
}; };
} }
const setIfReferrerData = first(
compact(setIfReferrer?.edges).map((e) => e.node)
);
const setIfRefereeData = first(
compact(setIfReferee?.edges).map((e) => e.node)
);
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
const referralSets = compact(setReferees.edges).map((e) => e.node);
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
const referralDiscount = Number(referralStats?.discountFactor || 0); const referralDiscount = Number(referralStats?.discountFactor || 0);
const referralVolumeInWindow = Number( const referralVolumeInWindow = Number(
referralStats?.referralSetRunningNotionalTakerVolume || 0 referralStats?.referralSetRunningNotionalTakerVolume || 0
); );
const referralTierIndex = referralStats const referralTierIndex = referralTiers.findIndex(
? getReferralBenefitTier( (tier) => tier.referralDiscountFactor === referralStats?.discountFactor
epochsInSet, );
Number(referralStats.referralSetRunningNotionalTakerVolume),
referralTiers
)
: -1;
return { return {
referralDiscount, referralDiscount,
referralVolumeInWindow, referralVolumeInWindow,
referralTierIndex, referralTierIndex,
referralTiers, referralTiers,
epochsInSet, epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
code: (setIfReferrerData || setIfRefereeData)?.id, code: (setIfReferrer || setIfReferee)?.id,
isReferrer: Boolean(setIfReferrerData), isReferrer: Boolean(setIfReferrer),
}; };
}; };

View File

@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react';
import { useVolumeStats } from './use-volume-stats'; import { useVolumeStats } from './use-volume-stats';
describe('useReferralStats', () => { describe('useReferralStats', () => {
const statsList = { const stats = {
edges: [ __typename: 'VolumeDiscountStats' as const,
{ atEpoch: 10,
__typename: 'VolumeDiscountStatsEdge' as const, discountFactor: '0.05',
node: { runningVolume: '200',
__typename: 'VolumeDiscountStats' as const,
atEpoch: 9,
discountFactor: '0.1',
runningVolume: '100',
},
},
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: '200',
},
},
],
}; };
const program = { const program = {
@ -44,7 +28,7 @@ describe('useReferralStats', () => {
}; };
it('returns correct default values', () => { it('returns correct default values', () => {
const { result } = renderHook(() => useVolumeStats()); const { result } = renderHook(() => useVolumeStats(10));
expect(result.current).toEqual({ expect(result.current).toEqual({
volumeDiscount: 0, volumeDiscount: 0,
volumeInWindow: 0, volumeInWindow: 0,
@ -53,11 +37,18 @@ describe('useReferralStats', () => {
}); });
}); });
it('returns formatted data and tiers', () => { it('returns default values if no stat is not from previous epoch', () => {
const { result } = renderHook(() => useVolumeStats(statsList, program)); const { result } = renderHook(() => useVolumeStats(11, stats, program));
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
volumeTierIndex: -1,
volumeTiers: program.benefitTiers,
});
});
// should use stats from latest epoch it('returns formatted data and tiers', () => {
const stats = statsList.edges[1].node; const { result } = renderHook(() => useVolumeStats(10, stats, program));
expect(result.current).toEqual({ expect(result.current).toEqual({
volumeDiscount: Number(stats.discountFactor), volumeDiscount: Number(stats.discountFactor),
@ -66,30 +57,4 @@ describe('useReferralStats', () => {
volumeTiers: program.benefitTiers, volumeTiers: program.benefitTiers,
}); });
}); });
it.each([
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '350', index: 2 },
])('returns index: $index for the running volume: $volume', (obj) => {
const statsA = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: obj.volume,
},
},
],
};
const { result } = renderHook(() => useVolumeStats(statsA, program));
expect(result.current.volumeTierIndex).toBe(obj.index);
});
}); });

View File

@ -1,15 +1,15 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getVolumeTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees'; import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useVolumeStats = ( export const useVolumeStats = (
stats?: FeesQuery['volumeDiscountStats'], previousEpoch: number,
lastEpochStats?: NonNullable<
FeesQuery['volumeDiscountStats']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentVolumeDiscountProgram'] program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
) => { ) => {
const volumeTiers = program?.benefitTiers || []; const volumeTiers = program?.benefitTiers || [];
if (!stats || !program) { if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
return { return {
volumeDiscount: 0, volumeDiscount: 0,
volumeTierIndex: -1, volumeTierIndex: -1,
@ -18,11 +18,11 @@ export const useVolumeStats = (
}; };
} }
const volumeStats = compact(stats.edges).map((e) => e.node);
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0); const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0); const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers); const volumeTierIndex = volumeTiers.findIndex(
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
);
return { return {
volumeDiscount, volumeDiscount,

View File

@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
return formatter.format(parseFloat(pct.toFixed(5))); return formatter.format(parseFloat(pct.toFixed(5)));
}; };
/**
* Return the index of the benefit tier for volume discounts. A user
* only needs to fulfill a minimum volume requirement for the tier
*/
export const getVolumeTier = (
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
}>
) => {
return tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
};
/**
* Return the index of the benefit tiers for referrals. A user must
* fulfill both the minimum epochs in the referral set, and the set
* must reach the combined total volume
*/
export const getReferralBenefitTier = (
epochsInSet: number,
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
minimumEpochs: number;
}>
) => {
const indexByEpoch = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validEpochs = epochsInSet >= tier.minimumEpochs;
if (nextTier) {
return validEpochs && epochsInSet < nextTier.minimumEpochs;
}
return validEpochs;
});
const indexByVolume = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
return Math.min(indexByEpoch, indexByVolume);
};
/** /**
* Given a set of fees and a set of discounts return * Given a set of fees and a set of discounts return
* the adjusted fee factor * the adjusted fee factor

View File

@ -4376,7 +4376,19 @@ export type Query = {
/** The last block process by the blockchain */ /** The last block process by the blockchain */
lastBlockHeight: Scalars['String']; lastBlockHeight: Scalars['String'];
/** /**
* Get ledger entries by asset, market, party, account type, transfer type within the given date range. * Get a list of ledger entries within the given date range. The date range is restricted to a maximum of 5 days.
* This query requests and sums the number of ledger entries from a given subset of accounts, specified via the 'filter' argument.
* It returns a time series - implemented as a list of AggregateLedgerEntry structs - with a row for every time
* the summed ledger entries of the set of specified accounts changes.
* Each account filter must contain no more than one party ID.
* At least one party ID must be specified in the from or to account filter.
*
* Entries can be filtered by:
* - the sending account (market ID, asset ID, account type)
* - receiving account (market ID, asset ID, account type)
* - sending AND receiving account
* - transfer type either in addition to the above filters or as a standalone option
*
* Note: The date range is restricted to any 5 days. * Note: The date range is restricted to any 5 days.
* If no start or end date is provided, only ledger entries from the last 5 days will be returned. * If no start or end date is provided, only ledger entries from the last 5 days will be returned.
* If a start and end date are provided, but the end date is more than 5 days after the start date, only data up to 5 days after the start date will be returned. * If a start and end date are provided, but the end date is more than 5 days after the start date, only data up to 5 days after the start date will be returned.