From 70d748fb15aba19a80010846121edf9f3752caf9 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:23:04 +0200 Subject: [PATCH 1/6] fix(trading): fills fees fixes for maker (#5405) --- libs/fills/src/lib/fills-table.spec.tsx | 226 +++++++++++++++--------- libs/fills/src/lib/fills-table.tsx | 72 +++++--- 2 files changed, 191 insertions(+), 107 deletions(-) diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx index 8c0d395ab..f2b721661 100644 --- a/libs/fills/src/lib/fills-table.spec.tsx +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -35,6 +35,7 @@ const defaultFill: PartialDeep = { }, createdAt: new Date('2022-02-02T14:00:00').toISOString(), }; + describe('FillsTable', () => { it('correct columns are rendered', async () => { // 7005-FILL-001 @@ -271,96 +272,147 @@ describe('FillsTable', () => { .find((c) => c.getAttribute('col-id') === 'size'); expect(sizeCell).toHaveTextContent('3,000,000,000'); }); -}); -describe('FeesDiscountBreakdownTooltip', () => { - it('shows all discounts', () => { - const data = generateFill({ - ...defaultFill, - buyer: { - id: partyId, - }, + describe('FeesDiscountBreakdownTooltip', () => { + it('shows all discounts', () => { + const data = generateFill({ + ...defaultFill, + buyer: { + id: partyId, + }, + }); + const props = { + data, + partyId, + value: data.market, + } as Parameters['0']; + const { container } = render(); + const dt = container.querySelectorAll('dt'); + const dd = container.querySelectorAll('dd'); + const expectedDt = [ + 'Infrastructure Fee', + 'Referral Discount', + 'Volume Discount', + 'Liquidity Fee', + 'Referral Discount', + 'Volume Discount', + 'Maker Fee', + 'Referral Discount', + 'Volume Discount', + ]; + const expectedDD = [ + '0.05 BTC', + '0.06 BTC', + '0.01 BTC', + '0.02 BTC', + '0.03 BTC', + '0.04 BTC', + ]; + expectedDt.forEach((label, i) => { + expect(dt[i]).toHaveTextContent(label); + }); + expectedDD.forEach((label, i) => { + expect(dd[i]).toHaveTextContent(label); + }); }); - const props = { - data, - partyId, - value: data.market, - } as Parameters['0']; - const { container } = render(); - const dt = container.querySelectorAll('dt'); - const dd = container.querySelectorAll('dd'); - const expectedDt = [ - 'Infrastructure Fee', - 'Referral Discount', - 'Volume Discount', - 'Liquidity Fee', - 'Referral Discount', - 'Volume Discount', - 'Maker Fee', - 'Referral Discount', - 'Volume Discount', - ]; - const expectedDD = [ - '0.05 BTC', - '0.06 BTC', - '0.01 BTC', - '0.02 BTC', - '0.03 BTC', - '0.04 BTC', - ]; - expectedDt.forEach((label, i) => { - expect(dt[i]).toHaveTextContent(label); + }); + + describe('getFeesBreakdown', () => { + it('should return correct fees breakdown for a taker', () => { + const fees = { + makerFee: '1000', + infrastructureFee: '2000', + liquidityFee: '3000', + }; + const expectedBreakdown = { + infrastructureFee: '2000', + liquidityFee: '3000', + makerFee: '1000', + totalFee: '6000', + }; + expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown); }); - expectedDD.forEach((label, i) => { - expect(dd[i]).toHaveTextContent(label); + + it('should return correct fees breakdown for a maker if market', () => { + const fees = { + makerFee: '1000', + infrastructureFee: '2000', + liquidityFee: '3000', + }; + const expectedBreakdown = { + infrastructureFee: '0', + liquidityFee: '0', + makerFee: '-1000', + totalFee: '-1000', + }; + expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a maker if market is active', () => { + const fees = { + makerFee: '1000', + infrastructureFee: '2000', + liquidityFee: '3000', + }; + const expectedBreakdown = { + infrastructureFee: '0', + liquidityFee: '0', + makerFee: '-1000', + totalFee: '-1000', + }; + expect( + getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE) + ).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a maker if the market is suspended', () => { + const fees = { + infrastructureFee: '2000', + liquidityFee: '3000', + makerFee: '0', + }; + const expectedBreakdown = { + infrastructureFee: '1000', + liquidityFee: '1500', + makerFee: '0', + totalFee: '2500', + }; + expect( + getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED) + ).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a taker if the market is suspended', () => { + const fees = { + infrastructureFee: '2000', + liquidityFee: '3000', + makerFee: '0', + }; + const expectedBreakdown = { + infrastructureFee: '1000', + liquidityFee: '1500', + makerFee: '0', + totalFee: '2500', + }; + expect( + getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED) + ).toEqual(expectedBreakdown); + }); + }); + + describe('getTotalFeesDiscounts', () => { + it('should return correct total value', () => { + const fees = { + infrastructureFeeReferralDiscount: '1', + infrastructureFeeVolumeDiscount: '2', + liquidityFeeReferralDiscount: '3', + liquidityFeeVolumeDiscount: '4', + makerFeeReferralDiscount: '5', + makerFeeVolumeDiscount: '6', + }; + expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual( + (1 + 2 + 3 + 4 + 5 + 6).toString() + ); }); }); }); - -describe('getFeesBreakdown', () => { - it('should return correct fees breakdown for a taker', () => { - const fees = { - makerFee: '1000', - infrastructureFee: '2000', - liquidityFee: '3000', - }; - const expectedBreakdown = { - infrastructureFee: '2000', - liquidityFee: '3000', - makerFee: '1000', - totalFee: '6000', - }; - expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown); - }); - - it('should return correct fees breakdown for a maker', () => { - const fees = { - makerFee: '1000', - infrastructureFee: '2000', - liquidityFee: '3000', - }; - const expectedBreakdown = { - infrastructureFee: '2000', - liquidityFee: '3000', - makerFee: '-1000', - totalFee: '4000', - }; - expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown); - }); -}); - -describe('getTotalFeesDiscounts', () => { - it('should return correct total value', () => { - const fees = { - infrastructureFeeReferralDiscount: '1', - infrastructureFeeVolumeDiscount: '2', - liquidityFeeReferralDiscount: '3', - liquidityFeeVolumeDiscount: '4', - makerFeeReferralDiscount: '5', - makerFeeVolumeDiscount: '6', - }; - expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual( - (1 + 2 + 3 + 4 + 5 + 6).toString() - ); - }); -}); diff --git a/libs/fills/src/lib/fills-table.tsx b/libs/fills/src/lib/fills-table.tsx index 718207f4f..10fe62644 100644 --- a/libs/fills/src/lib/fills-table.tsx +++ b/libs/fills/src/lib/fills-table.tsx @@ -1,10 +1,11 @@ import { useMemo } from 'react'; -import type { - AgGridReact, - AgGridReactProps, - AgReactUiProps, +import { + type AgGridReact, + type AgGridReactProps, + type AgReactUiProps, } from 'ag-grid-react'; -import type { ITooltipParams, ColDef } from 'ag-grid-community'; +import type { ColDef } from 'ag-grid-community'; +import type { ITooltipParams } from 'ag-grid-community'; import { addDecimal, addDecimalsFormatNumber, @@ -290,6 +291,7 @@ export const getRoleAndFees = ({ }) => { let role: Role; let fees; + if (data?.buyer.id === partyId) { if (data.aggressor === Schema.Side.SIDE_BUY) { role = TAKER; @@ -315,7 +317,16 @@ export const getRoleAndFees = ({ } else { return { role: '-', fees: undefined }; } - return { role, fees }; + + // We make the assumption that the market state is active if the maker fee is zero on both sides + // This needs to be updated when we have a way to get the correct market state when that fill happened from the API + // because the maker fee factor can be set to 0 via governance + const marketState = + data?.buyerFee.makerFee === data.sellerFee.makerFee && + new BigNumber(data?.buyerFee.makerFee).isZero() + ? Schema.MarketState.STATE_SUSPENDED + : Schema.MarketState.STATE_ACTIVE; + return { role, fees, marketState }; }; const FeesBreakdownTooltip = ({ @@ -329,16 +340,21 @@ const FeesBreakdownTooltip = ({ const asset = getAsset(market); - const { role, fees } = getRoleAndFees({ data, partyId }) ?? {}; + const { role, fees, marketState } = getRoleAndFees({ data, partyId }) ?? {}; if (!fees) return null; const { infrastructureFee, liquidityFee, makerFee, totalFee } = - getFeesBreakdown(role, fees); + getFeesBreakdown(role, fees, marketState); return (
+

+ {t('If the market was %s', [ + Schema.MarketStateMapping[marketState].toLowerCase(), + ])} +

{role === MAKER && ( <>

{t('The maker will receive the maker fee.')}

@@ -352,7 +368,7 @@ const FeesBreakdownTooltip = ({ {role === TAKER && (

{t('Fees to be paid by the taker.')}

)} - {role === '-' && ( + {(role === '-' || marketState === Schema.MarketState.STATE_SUSPENDED) && (

{t( 'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.' @@ -393,8 +409,8 @@ const FeesDiscountBreakdownTooltipItem = ({ }) => value && value !== '0' ? ( <> -

{label}
-
+
{label}
+
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
@@ -417,7 +433,7 @@ export const FeesDiscountBreakdownTooltip = ({ data-testid="fee-discount-breakdown-tooltip" className="max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white" > -
+
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' || (fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
{t('Infrastructure Fee')}
@@ -479,20 +495,36 @@ export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => { export const getFeesBreakdown = ( role: Role, - feesObj: TradeFeeFieldsFragment + feesObj: TradeFeeFieldsFragment, + marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE ) => { - const makerFee = - role === MAKER - ? new BigNumber(feesObj.makerFee).times(-1).toString() - : feesObj.makerFee; + // If market is in auction we assume maker fee is zero + const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE; - const infrastructureFee = feesObj.infrastructureFee; - const liquidityFee = feesObj.liquidityFee; + // If role is taker, then these are the fees to be paid + let { makerFee, infrastructureFee, liquidityFee } = feesObj; + + if (isMarketActive) { + if (role === MAKER) { + makerFee = new BigNumber(feesObj.makerFee).times(-1).toString(); + infrastructureFee = '0'; + liquidityFee = '0'; + } + } else { + // If market is suspended (in monitoring auction), then half of the fees are paid + infrastructureFee = new BigNumber(infrastructureFee) + .dividedBy(2) + .toString(); + liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString(); + // maker fee is already zero + makerFee = '0'; + } const totalFee = new BigNumber(infrastructureFee) .plus(makerFee) .plus(liquidityFee) .toString(); + return { infrastructureFee, liquidityFee, From 61471228aae03080247e6b60e885e7b2576e8f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Fri, 1 Dec 2023 17:34:05 +0100 Subject: [PATCH 2/6] fix(trading): use discount stats only from previous epoch (#5411) Co-authored-by: asiaznik --- .../referrals/referral-statistics.tsx | 2 +- .../components/fees-container/Fees.graphql | 16 +- .../fees-container/__generated__/Fees.ts | 13 +- .../fees-container/fees-container.tsx | 24 +-- .../fees-container/use-referral-stats.spec.ts | 149 +++--------------- .../fees-container/use-referral-stats.ts | 57 +++---- .../fees-container/use-volume-stats.spec.ts | 69 ++------ .../fees-container/use-volume-stats.ts | 16 +- .../components/fees-container/utils.ts | 67 -------- libs/types/src/__generated__/types.ts | 14 +- 10 files changed, 104 insertions(+), 323 deletions(-) diff --git a/apps/trading/client-pages/referrals/referral-statistics.tsx b/apps/trading/client-pages/referrals/referral-statistics.tsx index 628732687..4f62f1c3c 100644 --- a/apps/trading/client-pages/referrals/referral-statistics.tsx +++ b/apps/trading/client-pages/referrals/referral-statistics.tsx @@ -123,7 +123,7 @@ export const Statistics = ({ t.discountFactor === discountFactorValue ); 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 const epochsValue = !isNaN(currentEpoch) && refereeInfo?.atEpoch diff --git a/apps/trading/components/fees-container/Fees.graphql b/apps/trading/components/fees-container/Fees.graphql index f4e4f6b44..339edead6 100644 --- a/apps/trading/components/fees-container/Fees.graphql +++ b/apps/trading/components/fees-container/Fees.graphql @@ -16,18 +16,11 @@ query DiscountPrograms { } } -query Fees( - $partyId: ID! - $volumeDiscountEpochs: Int! - $referralDiscountEpochs: Int! -) { +query Fees($partyId: ID!) { epoch { id } - volumeDiscountStats( - partyId: $partyId - pagination: { last: $volumeDiscountEpochs } - ) { + volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) { edges { node { atEpoch @@ -59,10 +52,7 @@ query Fees( } } } - referralSetStats( - partyId: $partyId - pagination: { last: $referralDiscountEpochs } - ) { + referralSetStats(partyId: $partyId, pagination: { last: 1 }) { edges { node { atEpoch diff --git a/apps/trading/components/fees-container/__generated__/Fees.ts b/apps/trading/components/fees-container/__generated__/Fees.ts index aeb340c17..ef67344a7 100644 --- a/apps/trading/components/fees-container/__generated__/Fees.ts +++ b/apps/trading/components/fees-container/__generated__/Fees.ts @@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr export type FeesQueryVariables = Types.Exact<{ partyId: Types.Scalars['ID']; - volumeDiscountEpochs: Types.Scalars['Int']; - referralDiscountEpochs: Types.Scalars['Int']; }>; @@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType; export type DiscountProgramsQueryResult = Apollo.QueryResult; export const FeesDocument = gql` - query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) { + query Fees($partyId: ID!) { epoch { id } - volumeDiscountStats( - partyId: $partyId - pagination: {last: $volumeDiscountEpochs} - ) { + volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) { edges { node { atEpoch @@ -104,7 +99,7 @@ export const FeesDocument = gql` } } } - referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) { + referralSetStats(partyId: $partyId, pagination: {last: 1}) { edges { node { atEpoch @@ -129,8 +124,6 @@ export const FeesDocument = gql` * const { data, loading, error } = useFeesQuery({ * variables: { * partyId: // value for 'partyId' - * volumeDiscountEpochs: // value for 'volumeDiscountEpochs' - * referralDiscountEpochs: // value for 'referralDiscountEpochs' * }, * }); */ diff --git a/apps/trading/components/fees-container/fees-container.tsx b/apps/trading/components/fees-container/fees-container.tsx index 2e156b821..121fa871e 100644 --- a/apps/trading/components/fees-container/fees-container.tsx +++ b/apps/trading/components/fees-container/fees-container.tsx @@ -42,19 +42,19 @@ export const FeesContainer = () => { programData?.currentVolumeDiscountProgram?.windowLength || 1; const referralDiscountWindowLength = programData?.currentReferralProgram?.windowLength || 1; - const { data: feesData, loading: feesLoading } = useFeesQuery({ variables: { partyId: pubKey || '', - volumeDiscountEpochs: volumeDiscountWindowLength, - referralDiscountEpochs: referralDiscountWindowLength, }, - skip: !pubKey || !programData, + skip: !pubKey, }); + const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1; + const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } = useVolumeStats( - feesData?.volumeDiscountStats, + previousEpoch, + feesData?.volumeDiscountStats.edges?.[0]?.node, programData?.currentVolumeDiscountProgram ); @@ -67,12 +67,12 @@ export const FeesContainer = () => { code, isReferrer, } = useReferralStats( - feesData?.referralSetStats, - feesData?.referralSetReferees, + previousEpoch, + feesData?.referralSetStats.edges?.[0]?.node, + feesData?.referralSetReferees.edges?.[0]?.node, programData?.currentReferralProgram, - feesData?.epoch, - feesData?.referrer, - feesData?.referee + feesData?.referrer.edges?.[0]?.node, + feesData?.referee.edges?.[0]?.node ); const loading = paramsLoading || feesLoading || programLoading; @@ -460,7 +460,7 @@ const VolumeTiers = ({ {Array.from(tiers).map((tier, i) => { - const isUserTier = tiers.length - 1 - tierIndex === i; + const isUserTier = tierIndex === i; return ( @@ -513,7 +513,7 @@ const ReferralTiers = ({ {Array.from(tiers).map((t, i) => { - const isUserTier = tiers.length - 1 - tierIndex === i; + const isUserTier = tierIndex === i; const requiredVolume = Number(t.minimumRunningNotionalTakerVolume); let unlocksIn = null; diff --git a/apps/trading/components/fees-container/use-referral-stats.spec.ts b/apps/trading/components/fees-container/use-referral-stats.spec.ts index e38eb899d..b1f5ff098 100644 --- a/apps/trading/components/fees-container/use-referral-stats.spec.ts +++ b/apps/trading/components/fees-container/use-referral-stats.spec.ts @@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react'; import { useReferralStats } from './use-referral-stats'; describe('useReferralStats', () => { - const setStats = { - edges: [ - { - __typename: 'ReferralSetStatsEdge' as const, - node: { - __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 stat = { + __typename: 'ReferralSetStats' as const, + atEpoch: 9, + discountFactor: '0.01', + referralSetRunningNotionalTakerVolume: '100', }; - const sets = { - edges: [ - { - node: { - atEpoch: 3, - }, - }, - { - node: { - atEpoch: 4, - }, - }, - ], - }; - - const epoch = { - id: '10', + const set = { + atEpoch: 4, }; 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(() => - 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({ - referralDiscount: Number(stats.discountFactor), - referralVolumeInWindow: Number( - stats.referralSetRunningNotionalTakerVolume - ), - referralTierIndex: 1, + referralDiscount: 0, + referralVolumeInWindow: 0, + referralTierIndex: -1, referralTiers: program.benefitTiers, - epochsInSet: Number(epoch.id) - set.atEpoch, + epochsInSet: 0, code: undefined, isReferrer: false, }); }); - it.each([ - { 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, - }, - }, - ], - }; + it('returns formatted data and tiers', () => { const { result } = renderHook(() => - useReferralStats(statsA, setsA, program, epoch) + useReferralStats(9, stat, set, program) ); - expect(result.current.referralTierIndex).toEqual(obj.index); - }); - - it.each([ - { volume: '50', index: -1 }, - { volume: '100', index: 0 }, - { volume: '150', index: 0 }, - { volume: '200', index: 1 }, - { volume: '250', index: 1 }, - { volume: '300', index: 2 }, - { 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); + expect(result.current).toEqual({ + referralDiscount: Number(stat.discountFactor), + referralVolumeInWindow: Number( + stat.referralSetRunningNotionalTakerVolume + ), + referralTierIndex: 0, + referralTiers: program.benefitTiers, + epochsInSet: stat.atEpoch - set.atEpoch, + code: undefined, + isReferrer: false, + }); }); }); diff --git a/apps/trading/components/fees-container/use-referral-stats.ts b/apps/trading/components/fees-container/use-referral-stats.ts index 94821ed70..10d7df55e 100644 --- a/apps/trading/components/fees-container/use-referral-stats.ts +++ b/apps/trading/components/fees-container/use-referral-stats.ts @@ -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 { first } from 'lodash'; - export const useReferralStats = ( - setStats?: FeesQuery['referralSetStats'], - setReferees?: FeesQuery['referralSetReferees'], + previousEpoch?: number, + referralStats?: NonNullable< + FeesQuery['referralSetStats']['edges']['0'] + >['node'], + setReferees?: NonNullable< + FeesQuery['referralSetReferees']['edges']['0'] + >['node'], program?: DiscountProgramsQuery['currentReferralProgram'], - epoch?: FeesQuery['epoch'], - setIfReferrer?: FeesQuery['referrer'], - setIfReferee?: FeesQuery['referee'] + setIfReferrer?: NonNullable['node'], + setIfReferee?: NonNullable['node'] ) => { const referralTiers = program?.benefitTiers || []; - if (!setStats || !setReferees || !program || !epoch) { + if ( + !previousEpoch || + referralStats?.atEpoch !== previousEpoch || + !program || + !setReferees + ) { return { referralDiscount: 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 referralVolumeInWindow = Number( referralStats?.referralSetRunningNotionalTakerVolume || 0 ); - const referralTierIndex = referralStats - ? getReferralBenefitTier( - epochsInSet, - Number(referralStats.referralSetRunningNotionalTakerVolume), - referralTiers - ) - : -1; + const referralTierIndex = referralTiers.findIndex( + (tier) => tier.referralDiscountFactor === referralStats?.discountFactor + ); return { referralDiscount, referralVolumeInWindow, referralTierIndex, referralTiers, - epochsInSet, - code: (setIfReferrerData || setIfRefereeData)?.id, - isReferrer: Boolean(setIfReferrerData), + epochsInSet: referralStats.atEpoch - setReferees.atEpoch, + code: (setIfReferrer || setIfReferee)?.id, + isReferrer: Boolean(setIfReferrer), }; }; diff --git a/apps/trading/components/fees-container/use-volume-stats.spec.ts b/apps/trading/components/fees-container/use-volume-stats.spec.ts index b9c327054..a3bfb6afd 100644 --- a/apps/trading/components/fees-container/use-volume-stats.spec.ts +++ b/apps/trading/components/fees-container/use-volume-stats.spec.ts @@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react'; import { useVolumeStats } from './use-volume-stats'; describe('useReferralStats', () => { - const statsList = { - edges: [ - { - __typename: 'VolumeDiscountStatsEdge' as const, - node: { - __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 stats = { + __typename: 'VolumeDiscountStats' as const, + atEpoch: 10, + discountFactor: '0.05', + runningVolume: '200', }; const program = { @@ -44,7 +28,7 @@ describe('useReferralStats', () => { }; it('returns correct default values', () => { - const { result } = renderHook(() => useVolumeStats()); + const { result } = renderHook(() => useVolumeStats(10)); expect(result.current).toEqual({ volumeDiscount: 0, volumeInWindow: 0, @@ -53,11 +37,18 @@ describe('useReferralStats', () => { }); }); - it('returns formatted data and tiers', () => { - const { result } = renderHook(() => useVolumeStats(statsList, program)); + it('returns default values if no stat is not from previous epoch', () => { + 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 - const stats = statsList.edges[1].node; + it('returns formatted data and tiers', () => { + const { result } = renderHook(() => useVolumeStats(10, stats, program)); expect(result.current).toEqual({ volumeDiscount: Number(stats.discountFactor), @@ -66,30 +57,4 @@ describe('useReferralStats', () => { 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); - }); }); diff --git a/apps/trading/components/fees-container/use-volume-stats.ts b/apps/trading/components/fees-container/use-volume-stats.ts index 39c1b787a..81eef7200 100644 --- a/apps/trading/components/fees-container/use-volume-stats.ts +++ b/apps/trading/components/fees-container/use-volume-stats.ts @@ -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'; export const useVolumeStats = ( - stats?: FeesQuery['volumeDiscountStats'], + previousEpoch: number, + lastEpochStats?: NonNullable< + FeesQuery['volumeDiscountStats']['edges']['0'] + >['node'], program?: DiscountProgramsQuery['currentVolumeDiscountProgram'] ) => { const volumeTiers = program?.benefitTiers || []; - if (!stats || !program) { + if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) { return { volumeDiscount: 0, 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 volumeInWindow = Number(lastEpochStats?.runningVolume || 0); - const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers); + const volumeTierIndex = volumeTiers.findIndex( + (tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor + ); return { volumeDiscount, diff --git a/apps/trading/components/fees-container/utils.ts b/apps/trading/components/fees-container/utils.ts index 68b980035..3a1ae5703 100644 --- a/apps/trading/components/fees-container/utils.ts +++ b/apps/trading/components/fees-container/utils.ts @@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => { 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 * the adjusted fee factor diff --git a/libs/types/src/__generated__/types.ts b/libs/types/src/__generated__/types.ts index 4829a0429..9c90d8c21 100644 --- a/libs/types/src/__generated__/types.ts +++ b/libs/types/src/__generated__/types.ts @@ -4376,7 +4376,19 @@ export type Query = { /** The last block process by the blockchain */ 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. * 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. From a59f7dfd295c9bcc374e28ae4dd3dcccd11ca053 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Fri, 1 Dec 2023 18:34:22 +0200 Subject: [PATCH 3/6] fix(trading): fills fees maker discounts (#5406) --- libs/fills/src/lib/fills-table.spec.tsx | 111 +------------- libs/fills/src/lib/fills-table.tsx | 160 ++++----------------- libs/fills/src/lib/fills-utils.spec.ts | 183 ++++++++++++++++++++++++ libs/fills/src/lib/fills-utils.ts | 164 +++++++++++++++++++++ 4 files changed, 377 insertions(+), 241 deletions(-) create mode 100644 libs/fills/src/lib/fills-utils.spec.ts create mode 100644 libs/fills/src/lib/fills-utils.ts diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx index f2b721661..81136946b 100644 --- a/libs/fills/src/lib/fills-table.spec.tsx +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -4,14 +4,8 @@ import { getDateTimeFormat } from '@vegaprotocol/utils'; import * as Schema from '@vegaprotocol/types'; import type { PartialDeep } from 'type-fest'; import type { Trade } from './fills-data-provider'; -import { - FeesDiscountBreakdownTooltip, - FillsTable, - getFeesBreakdown, - getTotalFeesDiscounts, -} from './fills-table'; +import { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table'; import { generateFill } from './test-helpers'; -import type { TradeFeeFieldsFragment } from './__generated__/Fills'; const partyId = 'party-id'; const defaultFill: PartialDeep = { @@ -66,7 +60,7 @@ describe('FillsTable', () => { expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders); }); - it('formats cells correctly for buyer fill', async () => { + it('formats cells correctly for buyer fill for maker', async () => { const buyerFill = generateFill({ ...defaultFill, buyer: { @@ -90,7 +84,7 @@ describe('FillsTable', () => { '3.00 BTC', 'Maker', '2.00 BTC', - '0.27 BTC', + '0.09 BTC', getDateTimeFormat().format(new Date(buyerFill.createdAt)), '', // action column ]; @@ -316,103 +310,4 @@ describe('FillsTable', () => { }); }); }); - - describe('getFeesBreakdown', () => { - it('should return correct fees breakdown for a taker', () => { - const fees = { - makerFee: '1000', - infrastructureFee: '2000', - liquidityFee: '3000', - }; - const expectedBreakdown = { - infrastructureFee: '2000', - liquidityFee: '3000', - makerFee: '1000', - totalFee: '6000', - }; - expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown); - }); - - it('should return correct fees breakdown for a maker if market', () => { - const fees = { - makerFee: '1000', - infrastructureFee: '2000', - liquidityFee: '3000', - }; - const expectedBreakdown = { - infrastructureFee: '0', - liquidityFee: '0', - makerFee: '-1000', - totalFee: '-1000', - }; - expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown); - }); - - it('should return correct fees breakdown for a maker if market is active', () => { - const fees = { - makerFee: '1000', - infrastructureFee: '2000', - liquidityFee: '3000', - }; - const expectedBreakdown = { - infrastructureFee: '0', - liquidityFee: '0', - makerFee: '-1000', - totalFee: '-1000', - }; - expect( - getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE) - ).toEqual(expectedBreakdown); - }); - - it('should return correct fees breakdown for a maker if the market is suspended', () => { - const fees = { - infrastructureFee: '2000', - liquidityFee: '3000', - makerFee: '0', - }; - const expectedBreakdown = { - infrastructureFee: '1000', - liquidityFee: '1500', - makerFee: '0', - totalFee: '2500', - }; - expect( - getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED) - ).toEqual(expectedBreakdown); - }); - - it('should return correct fees breakdown for a taker if the market is suspended', () => { - const fees = { - infrastructureFee: '2000', - liquidityFee: '3000', - makerFee: '0', - }; - const expectedBreakdown = { - infrastructureFee: '1000', - liquidityFee: '1500', - makerFee: '0', - totalFee: '2500', - }; - expect( - getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED) - ).toEqual(expectedBreakdown); - }); - }); - - describe('getTotalFeesDiscounts', () => { - it('should return correct total value', () => { - const fees = { - infrastructureFeeReferralDiscount: '1', - infrastructureFeeVolumeDiscount: '2', - liquidityFeeReferralDiscount: '3', - liquidityFeeVolumeDiscount: '4', - makerFeeReferralDiscount: '5', - makerFeeVolumeDiscount: '6', - }; - expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual( - (1 + 2 + 3 + 4 + 5 + 6).toString() - ); - }); - }); }); diff --git a/libs/fills/src/lib/fills-table.tsx b/libs/fills/src/lib/fills-table.tsx index 10fe62644..1ca38cc90 100644 --- a/libs/fills/src/lib/fills-table.tsx +++ b/libs/fills/src/lib/fills-table.tsx @@ -30,19 +30,11 @@ import type { import { forwardRef } from 'react'; import BigNumber from 'bignumber.js'; import type { Trade } from './fills-data-provider'; -import type { - FillFieldsFragment, - TradeFeeFieldsFragment, -} from './__generated__/Fills'; import { FillActionsDropdown } from './fill-actions-dropdown'; import { getAsset } from '@vegaprotocol/markets'; +import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils'; -const TAKER = 'Taker'; -const MAKER = 'Maker'; - -export type Role = typeof TAKER | typeof MAKER | '-'; - -export type Props = (AgGridReactProps | AgReactUiProps) & { +type Props = (AgGridReactProps | AgReactUiProps) & { partyId: string; onMarketClick?: (marketId: string, metaKey?: boolean) => void; }; @@ -262,73 +254,13 @@ const formatFeeDiscount = (partyId: string) => { }: VegaValueFormatterParams) => { if (!market || !data) return '-'; const asset = getAsset(market); - const { fees } = getRoleAndFees({ data, partyId }); - if (!fees) return '-'; - - const total = getTotalFeesDiscounts(fees); - return addDecimalsFormatNumber(total, asset.decimals); + const { fees: roleFees, role } = getRoleAndFees({ data, partyId }); + if (!roleFees) return '-'; + const { totalFeeDiscount } = getFeesBreakdown(role, roleFees); + return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals); }; }; -export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => { - if (!feeObj) return true; - return ( - feeObj.liquidityFee === '0' && - feeObj.makerFee === '0' && - feeObj.infrastructureFee === '0' - ); -}; - -export const getRoleAndFees = ({ - data, - partyId, -}: { - data: Pick< - FillFieldsFragment, - 'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor' - >; - partyId?: string; -}) => { - let role: Role; - let fees; - - if (data?.buyer.id === partyId) { - if (data.aggressor === Schema.Side.SIDE_BUY) { - role = TAKER; - fees = data?.buyerFee; - } else if (data.aggressor === Schema.Side.SIDE_SELL) { - role = MAKER; - fees = data?.sellerFee; - } else { - role = '-'; - fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee; - } - } else if (data?.seller.id === partyId) { - if (data.aggressor === Schema.Side.SIDE_SELL) { - role = TAKER; - fees = data?.sellerFee; - } else if (data.aggressor === Schema.Side.SIDE_BUY) { - role = MAKER; - fees = data?.buyerFee; - } else { - role = '-'; - fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee; - } - } else { - return { role: '-', fees: undefined }; - } - - // We make the assumption that the market state is active if the maker fee is zero on both sides - // This needs to be updated when we have a way to get the correct market state when that fill happened from the API - // because the maker fee factor can be set to 0 via governance - const marketState = - data?.buyerFee.makerFee === data.sellerFee.makerFee && - new BigNumber(data?.buyerFee.makerFee).isZero() - ? Schema.MarketState.STATE_SUSPENDED - : Schema.MarketState.STATE_ACTIVE; - return { role, fees, marketState }; -}; - const FeesBreakdownTooltip = ({ data, value: market, @@ -350,11 +282,13 @@ const FeesBreakdownTooltip = ({ data-testid="fee-breakdown-tooltip" className="z-20 max-w-sm px-4 py-2 text-xs text-black border rounded bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word dark:text-white" > -

- {t('If the market was %s', [ - Schema.MarketStateMapping[marketState].toLowerCase(), - ])} -

+ {marketState && ( +

+ {t('If the market was %s', [ + Schema.MarketStateMapping[marketState].toLowerCase(), + ])} +

+ )} {role === MAKER && ( <>

{t('The maker will receive the maker fee.')}

@@ -425,9 +359,13 @@ export const FeesDiscountBreakdownTooltip = ({ } const asset = getAsset(data.market); - const { fees } = getRoleAndFees({ data, partyId }) ?? {}; - if (!fees) return null; - + const { + fees: roleFees, + marketState, + role, + } = getRoleAndFees({ data, partyId }) ?? {}; + if (!roleFees) return null; + const fees = getFeesBreakdown(role, roleFees, marketState); return (
+ +
{t('Total Fee Discount')}
+
); }; - -export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => { - return ( - BigInt(fees.infrastructureFeeReferralDiscount || '0') + - BigInt(fees.infrastructureFeeVolumeDiscount || '0') + - BigInt(fees.liquidityFeeReferralDiscount || '0') + - BigInt(fees.liquidityFeeVolumeDiscount || '0') + - BigInt(fees.makerFeeReferralDiscount || '0') + - BigInt(fees.makerFeeVolumeDiscount || '0') - ).toString(); -}; - -export const getFeesBreakdown = ( - role: Role, - feesObj: TradeFeeFieldsFragment, - marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE -) => { - // If market is in auction we assume maker fee is zero - const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE; - - // If role is taker, then these are the fees to be paid - let { makerFee, infrastructureFee, liquidityFee } = feesObj; - - if (isMarketActive) { - if (role === MAKER) { - makerFee = new BigNumber(feesObj.makerFee).times(-1).toString(); - infrastructureFee = '0'; - liquidityFee = '0'; - } - } else { - // If market is suspended (in monitoring auction), then half of the fees are paid - infrastructureFee = new BigNumber(infrastructureFee) - .dividedBy(2) - .toString(); - liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString(); - // maker fee is already zero - makerFee = '0'; - } - - const totalFee = new BigNumber(infrastructureFee) - .plus(makerFee) - .plus(liquidityFee) - .toString(); - - return { - infrastructureFee, - liquidityFee, - makerFee, - totalFee, - }; -}; diff --git a/libs/fills/src/lib/fills-utils.spec.ts b/libs/fills/src/lib/fills-utils.spec.ts new file mode 100644 index 000000000..fd62ebd18 --- /dev/null +++ b/libs/fills/src/lib/fills-utils.spec.ts @@ -0,0 +1,183 @@ +import { getFeesBreakdown } from './fills-utils'; +import * as Schema from '@vegaprotocol/types'; + +describe('getFeesBreakdown', () => { + it('should return correct fees breakdown for a taker', () => { + const fees = { + makerFee: '1000', + infrastructureFee: '2000', + liquidityFee: '3000', + }; + const expectedBreakdown = { + infrastructureFee: '2000', + liquidityFee: '3000', + makerFee: '1000', + totalFee: '6000', + totalFeeDiscount: '0', + }; + expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a maker if market is active', () => { + const fees = { + makerFee: '1000', + infrastructureFee: '2000', + liquidityFee: '3000', + }; + const expectedBreakdown = { + infrastructureFee: '0', + liquidityFee: '0', + makerFee: '-1000', + totalFee: '-1000', + totalFeeDiscount: '0', + }; + expect( + getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE) + ).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a maker if the market is suspended', () => { + const fees = { + infrastructureFee: '2000', + liquidityFee: '3000', + makerFee: '0', + }; + const expectedBreakdown = { + infrastructureFee: '1000', + liquidityFee: '1500', + makerFee: '0', + totalFee: '2500', + totalFeeDiscount: '0', + }; + expect( + getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED) + ).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a taker if the market is suspended', () => { + const fees = { + infrastructureFee: '2000', + liquidityFee: '3000', + makerFee: '0', + }; + const expectedBreakdown = { + infrastructureFee: '1000', + liquidityFee: '1500', + makerFee: '0', + totalFee: '2500', + totalFeeDiscount: '0', + }; + expect( + getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED) + ).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a taker if market is active', () => { + const fees = { + makerFee: '1000', + infrastructureFee: '2000', + liquidityFee: '3000', + }; + const expectedBreakdown = { + infrastructureFee: '2000', + liquidityFee: '3000', + makerFee: '1000', + totalFee: '6000', + totalFeeDiscount: '0', + }; + expect( + getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_ACTIVE) + ).toEqual(expectedBreakdown); + }); + + it('should return correct fees breakdown for a maker', () => { + const fees = { + makerFee: '1000', + infrastructureFee: '2000', + liquidityFee: '3000', + }; + const expectedBreakdown = { + infrastructureFee: '0', + liquidityFee: '0', + makerFee: '-1000', + totalFee: '-1000', + totalFeeDiscount: '0', + }; + expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown); + }); + + it('should return correct total fees discount value for a taker (if the market is active - default)', () => { + const fees = { + infrastructureFeeReferralDiscount: '1', + infrastructureFeeVolumeDiscount: '2', + liquidityFeeReferralDiscount: '3', + liquidityFeeVolumeDiscount: '4', + makerFeeReferralDiscount: '5', + makerFeeVolumeDiscount: '6', + infrastructureFee: '1000', + liquidityFee: '2000', + makerFee: '3000', + }; + const { totalFeeDiscount } = getFeesBreakdown('Taker', fees); + expect(totalFeeDiscount).toEqual((1 + 2 + 3 + 4 + 5 + 6).toString()); + }); + + it('should return correct total fees discount value for a maker (if the market is active - default)', () => { + const fees = { + infrastructureFeeReferralDiscount: '1', + infrastructureFeeVolumeDiscount: '2', + liquidityFeeReferralDiscount: '3', + liquidityFeeVolumeDiscount: '4', + makerFeeReferralDiscount: '5', + makerFeeVolumeDiscount: '6', + infrastructureFee: '1000', + liquidityFee: '2000', + makerFee: '3000', + }; + const { totalFeeDiscount } = getFeesBreakdown('Maker', fees); + // makerFeeReferralDiscount and makerFeeVolumeDiscount are added, infra and liq. fees are zeroed + expect(totalFeeDiscount).toEqual((5 + 6).toString()); + }); + + it('should return correct total fees discount value for a maker (if the market is suspended)', () => { + const fees = { + infrastructureFeeReferralDiscount: '1', + infrastructureFeeVolumeDiscount: '2', + liquidityFeeReferralDiscount: '3', + liquidityFeeVolumeDiscount: '4', + makerFeeReferralDiscount: '5', + makerFeeVolumeDiscount: '6', + infrastructureFee: '1000', + liquidityFee: '2000', + makerFee: '3000', + }; + const { totalFeeDiscount } = getFeesBreakdown( + 'Maker', + fees, + Schema.MarketState.STATE_SUSPENDED + ); + // makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved + expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString()); + }); + + it('should return correct total fees discount value for a taker (if the market is suspended)', () => { + const fees = { + infrastructureFeeReferralDiscount: '1', + infrastructureFeeVolumeDiscount: '2', + liquidityFeeReferralDiscount: '3', + liquidityFeeVolumeDiscount: '4', + makerFeeReferralDiscount: '5', + makerFeeVolumeDiscount: '6', + infrastructureFee: '1000', + liquidityFee: '2000', + makerFee: '3000', + }; + const { totalFeeDiscount } = getFeesBreakdown( + 'Taker', + fees, + Schema.MarketState.STATE_SUSPENDED + ); + // makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved + expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString()); + }); +}); diff --git a/libs/fills/src/lib/fills-utils.ts b/libs/fills/src/lib/fills-utils.ts new file mode 100644 index 000000000..d5fc0b47a --- /dev/null +++ b/libs/fills/src/lib/fills-utils.ts @@ -0,0 +1,164 @@ +import BigNumber from 'bignumber.js'; +import type { + FillFieldsFragment, + TradeFeeFieldsFragment, +} from './__generated__/Fills'; +import * as Schema from '@vegaprotocol/types'; + +export const TAKER = 'Taker'; +export const MAKER = 'Maker'; + +export type Role = typeof TAKER | typeof MAKER | '-'; + +export const getRoleAndFees = ({ + data, + partyId, +}: { + data: Pick< + FillFieldsFragment, + 'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor' + >; + partyId?: string; +}): { + role: Role; + fees?: TradeFeeFieldsFragment; + marketState?: Schema.MarketState; +} => { + let role: Role; + let fees; + + if (data?.buyer.id === partyId) { + if (data.aggressor === Schema.Side.SIDE_BUY) { + role = TAKER; + fees = data?.buyerFee; + } else if (data.aggressor === Schema.Side.SIDE_SELL) { + role = MAKER; + fees = data?.sellerFee; + } else { + role = '-'; + fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee; + } + } else if (data?.seller.id === partyId) { + if (data.aggressor === Schema.Side.SIDE_SELL) { + role = TAKER; + fees = data?.sellerFee; + } else if (data.aggressor === Schema.Side.SIDE_BUY) { + role = MAKER; + fees = data?.buyerFee; + } else { + role = '-'; + fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee; + } + } else { + return { role: '-', fees: undefined }; + } + + // We make the assumption that the market state is active if the maker fee is zero on both sides + // This needs to be updated when we have a way to get the correct market state when that fill happened from the API + // because the maker fee factor can be set to 0 via governance + const marketState = + data?.buyerFee.makerFee === data.sellerFee.makerFee && + new BigNumber(data?.buyerFee.makerFee).isZero() + ? Schema.MarketState.STATE_SUSPENDED + : Schema.MarketState.STATE_ACTIVE; + return { role, fees, marketState }; +}; + +export const getFeesBreakdown = ( + role: Role, + fees: TradeFeeFieldsFragment, + marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE +) => { + // If market is in auction we assume maker fee is zero + const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE; + + // If role is taker, then these are the fees to be paid + let { makerFee, infrastructureFee, liquidityFee } = fees; + // If role is taker, then these are the fees discounts to be applied + let { + makerFeeVolumeDiscount, + makerFeeReferralDiscount, + infrastructureFeeVolumeDiscount, + infrastructureFeeReferralDiscount, + liquidityFeeVolumeDiscount, + liquidityFeeReferralDiscount, + } = fees; + + if (isMarketActive) { + if (role === MAKER) { + makerFee = new BigNumber(fees.makerFee).times(-1).toString(); + infrastructureFee = '0'; + liquidityFee = '0'; + + // discounts are also zero or we can leave them undefined + infrastructureFeeReferralDiscount = + infrastructureFeeReferralDiscount && '0'; + infrastructureFeeVolumeDiscount = infrastructureFeeVolumeDiscount && '0'; + liquidityFeeReferralDiscount = liquidityFeeReferralDiscount && '0'; + liquidityFeeVolumeDiscount = liquidityFeeVolumeDiscount && '0'; + + // we leave maker discount fees as they are defined + } + } else { + // If market is suspended (in monitoring auction), then half of the fees are paid + infrastructureFee = new BigNumber(infrastructureFee) + .dividedBy(2) + .toString(); + liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString(); + // maker fee is already zero + makerFee = '0'; + + // discounts are also halved + infrastructureFeeReferralDiscount = + infrastructureFeeReferralDiscount && + new BigNumber(infrastructureFeeReferralDiscount).dividedBy(2).toString(); + infrastructureFeeVolumeDiscount = + infrastructureFeeVolumeDiscount && + new BigNumber(infrastructureFeeVolumeDiscount).dividedBy(2).toString(); + liquidityFeeReferralDiscount = + liquidityFeeReferralDiscount && + new BigNumber(liquidityFeeReferralDiscount).dividedBy(2).toString(); + liquidityFeeVolumeDiscount = + liquidityFeeVolumeDiscount && + new BigNumber(liquidityFeeVolumeDiscount).dividedBy(2).toString(); + // maker discount fees should already be zero + makerFeeReferralDiscount = makerFeeReferralDiscount && '0'; + makerFeeVolumeDiscount = makerFeeVolumeDiscount && '0'; + } + + const totalFee = new BigNumber(infrastructureFee) + .plus(makerFee) + .plus(liquidityFee) + .toString(); + + const totalFeeDiscount = new BigNumber(makerFeeVolumeDiscount || '0') + .plus(makerFeeReferralDiscount || '0') + .plus(infrastructureFeeReferralDiscount || '0') + .plus(infrastructureFeeVolumeDiscount || '0') + .plus(liquidityFeeReferralDiscount || '0') + .plus(liquidityFeeVolumeDiscount || '0') + .toString(); + + return { + infrastructureFee, + infrastructureFeeReferralDiscount, + infrastructureFeeVolumeDiscount, + liquidityFee, + liquidityFeeReferralDiscount, + liquidityFeeVolumeDiscount, + makerFee, + makerFeeReferralDiscount, + makerFeeVolumeDiscount, + totalFee, + totalFeeDiscount, + }; +}; + +export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => { + if (!feeObj) return true; + return ( + feeObj.liquidityFee === '0' && + feeObj.makerFee === '0' && + feeObj.infrastructureFee === '0' + ); +}; From 8a3657a9b9d5f64d814e553354a2c0ae2dffb01a Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Sat, 2 Dec 2023 13:06:20 +0200 Subject: [PATCH 4/6] fix(trading): live time fraction zero (#5419) --- libs/liquidity/src/lib/liquidity-table.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/libs/liquidity/src/lib/liquidity-table.tsx b/libs/liquidity/src/lib/liquidity-table.tsx index b34c0a464..1446960ac 100644 --- a/libs/liquidity/src/lib/liquidity-table.tsx +++ b/libs/liquidity/src/lib/liquidity-table.tsx @@ -412,14 +412,9 @@ export const LiquidityTable = ({ }, 'text-red-500': ({ data }: { data: LiquidityProvisionData }) => { if (!data.sla) return false; - return ( - new BigNumber( - data.sla.currentEpochFractionOfTimeOnBook - ).isLessThan(data.commitmentMinTimeFraction) && - new BigNumber( - data.sla.currentEpochFractionOfTimeOnBook - ).isGreaterThan(0) - ); + return new BigNumber( + data.sla.currentEpochFractionOfTimeOnBook + ).isLessThan(data.commitmentMinTimeFraction); }, }, }, From 51ab02a2e2b44cbca3e0b31306ad2d4469561a02 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:36:02 +0200 Subject: [PATCH 5/6] fix(trading): missing party ID when there is no SLA data (#5446) Co-authored-by: Matthew Russell --- .../src/lib/liquidity-data-provider.spec.tsx | 2 ++ libs/liquidity/src/lib/liquidity-data-provider.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx b/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx index 6e9098502..19be5228d 100644 --- a/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx +++ b/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx @@ -119,6 +119,8 @@ describe('getLiquidityProvision', () => { createdAt: '2022-12-16T09:28:29.071781Z', id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59', fee: '0.001', + partyId: + 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59', party: { __typename: 'Party', accountsConnection: { diff --git a/libs/liquidity/src/lib/liquidity-data-provider.ts b/libs/liquidity/src/lib/liquidity-data-provider.ts index 6ab1744f8..cabd4104e 100644 --- a/libs/liquidity/src/lib/liquidity-data-provider.ts +++ b/libs/liquidity/src/lib/liquidity-data-provider.ts @@ -5,17 +5,15 @@ import { } from '@vegaprotocol/data-provider'; import * as Schema from '@vegaprotocol/types'; import BigNumber from 'bignumber.js'; - import { LiquidityProvidersDocument, LiquidityProvisionsDocument, } from './__generated__/MarketLiquidity'; - import type { LiquidityProviderFieldsFragment, + LiquidityProvisionFieldsFragment, LiquidityProvidersQuery, LiquidityProvidersQueryVariables, - LiquidityProvisionFieldsFragment, LiquidityProvisionsQuery, LiquidityProvisionsQueryVariables, } from './__generated__/MarketLiquidity'; @@ -163,7 +161,14 @@ export const getLiquidityProvision = ( const liquidityProvider = liquidityProviders.find( (f) => liquidityProvision.party.id === f.partyId ); - if (!liquidityProvider) return liquidityProvision; + + if (!liquidityProvider) { + return { + ...liquidityProvision, + partyId: liquidityProvision.party.id, + }; + } + const accounts = compact( liquidityProvision.party.accountsConnection?.edges ).map((e) => e.node); From eb81f4ae44124d10cf9d3b9c233eb3610d8d3c5b Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:36:25 +0200 Subject: [PATCH 6/6] fix(trading): required vol shown if current is zero (#5449) --- .../components/fees-container/fees-container.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/trading/components/fees-container/fees-container.tsx b/apps/trading/components/fees-container/fees-container.tsx index 121fa871e..c09f43b2f 100644 --- a/apps/trading/components/fees-container/fees-container.tsx +++ b/apps/trading/components/fees-container/fees-container.tsx @@ -312,16 +312,23 @@ export const CurrentVolume = ({ }) => { const nextTier = tiers[tierIndex + 1]; const requiredForNextTier = nextTier - ? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume - : 0; + ? new BigNumber(nextTier.minimumRunningNotionalTakerVolume).minus( + windowLengthVolume + ) + : new BigNumber(0); + const currentVolume = new BigNumber(windowLengthVolume); return (
- {requiredForNextTier > 0 && ( + {requiredForNextTier.isGreaterThan(0) && (