Compare commits

...
11 changed files with 368 additions and 101 deletions
@@ -26,11 +26,7 @@ import {
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import {
sumFees,
sumFeesDiscounts,
useEstimateFees,
} from '../../hooks/use-estimate-fees';
import { useEstimateFees } from '../../hooks/use-estimate-fees';
import { KeyValue } from './key-value';
import {
Accordion,
@@ -44,6 +40,7 @@ import {
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { FeesBreakdown } from '../fees-breakdown';
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
const emptyValue = '-';
@@ -63,48 +60,49 @@ export const DealTicketFeeDetails = ({
const feeEstimate = useEstimateFees(order, isMarketInAuction);
const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset;
const totalFees = feeEstimate?.fees && sumFees(feeEstimate?.fees);
const feesDiscounts =
feeEstimate?.fees && sumFeesDiscounts(feeEstimate?.fees);
const totalPercentageDiscount =
feesDiscounts &&
totalFees &&
feesDiscounts.total !== '0' &&
totalFees !== '0' &&
new BigNumber(feesDiscounts.total)
.dividedBy(BigNumber.sum(totalFees, feesDiscounts.total))
.times(100);
const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
const totalDiscountedFeeAmount =
feeEstimate?.totalFeeAmount &&
getDiscountedFee(
feeEstimate.totalFeeAmount,
feeEstimate.referralDiscountFactor,
feeEstimate.volumeDiscountFactor
).discountedFee;
return (
<KeyValue
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
}
formattedValue={
<>
{totalPercentageDiscount && (
{totalDiscountFactor && (
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
-{formatNumberPercentage(totalPercentageDiscount, 2)}
-
{formatNumberPercentage(
new BigNumber(totalDiscountFactor).multipliedBy(100),
2
)}
</Pill>
)}
{feeEstimate?.totalFeeAmount &&
`~${formatValue(
feeEstimate?.totalFeeAmount,
assetDecimals,
quantum
)}`}
{totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
</>
}
labelDescription={
<>
<span>
<p className="mb-2">
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.`
)}
</span>
</p>
<FeesBreakdown
totalFeeAmount={feeEstimate?.totalFeeAmount}
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
@@ -0,0 +1,66 @@
import { getDiscountedFee, getTotalDiscountFactor } from './discounts';
describe('getDiscountedFee', () => {
it('calculates values if volumeDiscount or referralDiscount is undefined', () => {
expect(getDiscountedFee('100')).toEqual({
discountedFee: '100',
volumeDiscount: '0',
referralDiscount: '0',
});
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
discountedFee: '90',
volumeDiscount: '10',
referralDiscount: '0',
});
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
discountedFee: '90',
volumeDiscount: '0',
referralDiscount: '10',
});
});
it('calculates values using volumeDiscount or referralDiscount', () => {
expect(getDiscountedFee('', '0.1', '0.2')).toEqual({
discountedFee: '',
volumeDiscount: '0',
referralDiscount: '0',
});
});
});
describe('getTotalDiscountFactor', () => {
it('returns 0 if discounts are 0', () => {
expect(
getTotalDiscountFactor({
volumeDiscountFactor: '0',
referralDiscountFactor: '0',
})
).toEqual(0);
});
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
expect(
getTotalDiscountFactor({
volumeDiscountFactor: '0.1',
referralDiscountFactor: '0',
})
).toEqual(0.1);
});
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
expect(
getTotalDiscountFactor({
volumeDiscountFactor: '0',
referralDiscountFactor: '0.1',
})
).toEqual(0.1);
});
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
expect(
getTotalDiscountFactor({
volumeDiscountFactor: '0.2',
referralDiscountFactor: '0.1',
})
).toBeCloseTo(0.28);
});
});
@@ -0,0 +1,54 @@
import BigNumber from 'bignumber.js';
export const getDiscountedFee = (
feeAmount: string,
referralDiscountFactor?: string,
volumeDiscountFactor?: string
) => {
if (
((!referralDiscountFactor || referralDiscountFactor === '0') &&
(!volumeDiscountFactor || volumeDiscountFactor === '0')) ||
!feeAmount ||
feeAmount === '0'
) {
return {
discountedFee: feeAmount,
volumeDiscount: '0',
referralDiscount: '0',
};
}
const referralDiscount = new BigNumber(referralDiscountFactor || '0')
.multipliedBy(feeAmount)
.toFixed(0, BigNumber.ROUND_FLOOR);
const volumeDiscount = new BigNumber(volumeDiscountFactor || '0')
.multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString())
.toFixed(0, BigNumber.ROUND_FLOOR);
const discountedFee = (
BigInt(feeAmount || '0') -
BigInt(referralDiscount) -
BigInt(volumeDiscount)
).toString();
return {
referralDiscount,
volumeDiscount,
discountedFee,
};
};
export const getTotalDiscountFactor = (feeEstimate?: {
volumeDiscountFactor?: string;
referralDiscountFactor?: string;
}) => {
if (!feeEstimate) {
return 0;
}
const volumeFactor = Number(feeEstimate?.volumeDiscountFactor) || 0;
const referralFactor = Number(feeEstimate?.referralDiscountFactor) || 0;
if (!volumeFactor) {
return referralFactor;
}
if (!referralFactor) {
return volumeFactor;
}
return 1 - (1 - volumeFactor) * (1 - referralFactor);
};
@@ -6,7 +6,7 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import BigNumber from 'bignumber.js';
import { sumFees, sumFeesDiscounts } from '../../hooks';
import { getDiscountedFee } from '../discounts';
const formatValue = (
value: string | number | null | undefined,
@@ -24,7 +24,7 @@ const FeesBreakdownItem = ({
decimals,
}: {
label: string;
factor?: BigNumber;
factor?: string;
value: string;
symbol?: string;
decimals: number;
@@ -43,76 +43,86 @@ const FeesBreakdownItem = ({
);
export const FeesBreakdown = ({
totalFeeAmount,
fees,
feeFactors,
symbol,
decimals,
referralDiscountFactor,
volumeDiscountFactor,
}: {
totalFeeAmount?: string;
fees?: TradeFee;
feeFactors?: FeeFactors;
symbol?: string;
decimals: number;
referralDiscountFactor?: string;
volumeDiscountFactor?: string;
}) => {
if (!fees) return null;
const totalFees = sumFees(fees);
const {
total: totalDiscount,
referral: referralDiscount,
volume: volumeDiscount,
} = sumFeesDiscounts(fees);
if (totalFees === '0') return null;
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee(
fees.infrastructureFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { discountedFee: discountedLiquidityFee } = getDiscountedFee(
fees.liquidityFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { discountedFee: discountedMakerFee } = getDiscountedFee(
fees.makerFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { volumeDiscount, referralDiscount } = getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
);
return (
<dl className="grid grid-cols-6">
<FeesBreakdownItem
label={t('Infrastructure fee')}
factor={
feeFactors?.infrastructureFee
? new BigNumber(feeFactors?.infrastructureFee)
: undefined
}
value={fees.infrastructureFee}
factor={feeFactors?.infrastructureFee}
value={discountedInfrastructureFee}
symbol={symbol}
decimals={decimals}
/>
<FeesBreakdownItem
label={t('Liquidity fee')}
factor={
feeFactors?.liquidityFee
? new BigNumber(feeFactors?.liquidityFee)
: undefined
}
value={fees.liquidityFee}
factor={feeFactors?.liquidityFee}
value={discountedLiquidityFee}
symbol={symbol}
decimals={decimals}
/>
<FeesBreakdownItem
label={t('Maker fee')}
factor={
feeFactors?.makerFee ? new BigNumber(feeFactors?.makerFee) : undefined
}
value={fees.makerFee}
factor={feeFactors?.makerFee}
value={discountedMakerFee}
symbol={symbol}
decimals={decimals}
/>
{volumeDiscount && volumeDiscount !== '0' && (
{volumeDiscountFactor && volumeDiscount !== '0' && (
<FeesBreakdownItem
label={t('Volume discount')}
factor={new BigNumber(volumeDiscount).dividedBy(
BigNumber.sum(totalFees, totalDiscount)
)}
factor={volumeDiscountFactor}
value={volumeDiscount}
symbol={symbol}
decimals={decimals}
/>
)}
{referralDiscount && referralDiscount !== '0' && (
{referralDiscountFactor && referralDiscount !== '0' && (
<FeesBreakdownItem
label={t('Referral discount')}
factor={new BigNumber(referralDiscount).dividedBy(
BigNumber.sum(totalFees, totalDiscount)
)}
factor={referralDiscountFactor}
value={referralDiscount}
symbol={symbol}
decimals={decimals}
@@ -120,8 +130,8 @@ export const FeesBreakdown = ({
)}
<FeesBreakdownItem
label={t('Total fees')}
factor={feeFactors ? sumFeesFactors(feeFactors) : undefined}
value={totalFees}
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
value={totalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
@@ -31,4 +31,25 @@ query EstimateFees(
}
totalFeeAmount
}
epoch {
id
}
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
edges {
node {
atEpoch
discountFactor
runningVolume
}
}
}
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
edges {
node {
atEpoch
discountFactor
referralSetRunningNotionalTakerVolume
}
}
}
}
+22 -1
View File
@@ -15,7 +15,7 @@ export type EstimateFeesQueryVariables = Types.Exact<{
}>;
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } };
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } }, epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
export const EstimateFeesDocument = gql`
@@ -43,6 +43,27 @@ export const EstimateFeesDocument = gql`
}
totalFeeAmount
}
epoch {
id
}
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
edges {
node {
atEpoch
discountFactor
runningVolume
}
}
}
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
edges {
node {
atEpoch
discountFactor
referralSetRunningNotionalTakerVolume
}
}
}
}
`;
@@ -6,6 +6,15 @@ export const estimateFeesQuery = (
override?: PartialDeep<EstimateFeesQuery>
): EstimateFeesQuery => {
const defaultResult: EstimateFeesQuery = {
epoch: {
id: '1',
},
referralSetStats: {
edges: [],
},
volumeDiscountStats: {
edges: [],
},
estimateFees: {
__typename: 'FeeEstimate',
totalFeeAmount: '0.0006',
@@ -5,6 +5,31 @@ import { Side, OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
const data: EstimateFeesQuery = {
epoch: {
id: '2',
},
volumeDiscountStats: {
edges: [
{
node: {
atEpoch: 1,
discountFactor: '0.1',
runningVolume: '100',
},
},
],
},
referralSetStats: {
edges: [
{
node: {
atEpoch: 1,
discountFactor: '0.2',
referralSetRunningNotionalTakerVolume: '100',
},
},
],
},
estimateFees: {
totalFeeAmount: '120',
fees: {
@@ -54,6 +79,8 @@ describe('useEstimateFees', () => {
liquidityFee: '0',
makerFee: '0',
},
referralDiscountFactor: '0',
volumeDiscountFactor: '0',
});
expect(mockUseEstimateFeesQuery.mock.lastCall?.[0].skip).toBeTruthy();
});
@@ -85,6 +112,46 @@ describe('useEstimateFees', () => {
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
},
referralDiscountFactor: '0',
volumeDiscountFactor: '0',
});
});
it('returns 0 discounts if discount stats are not at the current epoch', () => {
const { result } = renderHook(() =>
useEstimateFees(
{
marketId: 'marketId',
side: Side.SIDE_BUY,
size: '1',
price: '1',
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
type: OrderType.TYPE_LIMIT,
},
true
)
);
expect(result.current?.referralDiscountFactor).toEqual('0');
expect(result.current?.volumeDiscountFactor).toEqual('0');
});
it('returns discounts', () => {
data.epoch.id = '1';
const { result } = renderHook(() =>
useEstimateFees(
{
marketId: 'marketId',
side: Side.SIDE_BUY,
size: '1',
price: '1',
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
type: OrderType.TYPE_LIMIT,
},
true
)
);
expect(result.current?.referralDiscountFactor).toEqual('0.2');
expect(result.current?.volumeDiscountFactor).toEqual('0.1');
});
});
@@ -5,39 +5,22 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
const divideByTwo = (n: string) => (BigInt(n) / BigInt(2)).toString();
export const sumFeesDiscounts = (
fees: EstimateFeesQuery['estimateFees']['fees']
) => {
const volume = (
BigInt(fees.makerFeeVolumeDiscount || '0') +
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
BigInt(fees.liquidityFeeVolumeDiscount || '0')
).toString();
const referral = (
BigInt(fees.makerFeeReferralDiscount || '0') +
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
BigInt(fees.liquidityFeeReferralDiscount || '0')
).toString();
return {
volume,
referral,
total: (BigInt(volume) + BigInt(referral)).toString(),
};
};
export const sumFees = (fees: EstimateFeesQuery['estimateFees']['fees']) =>
(
BigInt(fees.makerFee || '0') +
BigInt(fees.infrastructureFee || '0') +
BigInt(fees.liquidityFee || '0')
).toString();
export const useEstimateFees = (
order?: OrderSubmissionBody['orderSubmission'],
isMarketInAuction?: boolean
): EstimateFeesQuery['estimateFees'] | undefined => {
):
| (EstimateFeesQuery['estimateFees'] & {
referralDiscountFactor: string;
volumeDiscountFactor: string;
})
| undefined => {
const { pubKey } = useVegaWallet();
const { data } = useEstimateFeesQuery({
const {
data: currentData,
previousData,
loading,
} = useEstimateFeesQuery({
variables: order && {
marketId: order.marketId,
partyId: pubKey || '',
@@ -50,8 +33,21 @@ export const useEstimateFees = (
fetchPolicy: 'no-cache',
skip: !pubKey || !order?.size || !order?.price || order.postOnly,
});
const data = loading ? currentData || previousData : currentData;
const volumeDiscountFactor =
(data?.volumeDiscountStats.edges[0]?.node.atEpoch.toString() ===
data?.epoch.id &&
data?.volumeDiscountStats.edges[0]?.node.discountFactor) ||
'0';
const referralDiscountFactor =
(data?.referralSetStats.edges[0]?.node.atEpoch.toString() ===
data?.epoch.id &&
data?.referralSetStats.edges[0]?.node.discountFactor) ||
'0';
if (order?.postOnly) {
return {
volumeDiscountFactor,
referralDiscountFactor,
totalFeeAmount: '0',
fees: {
infrastructureFee: '0',
@@ -60,8 +56,13 @@ export const useEstimateFees = (
},
};
}
return isMarketInAuction && data?.estimateFees
if (!data?.estimateFees) {
return undefined;
}
return isMarketInAuction
? {
volumeDiscountFactor,
referralDiscountFactor,
totalFeeAmount: divideByTwo(data.estimateFees.totalFeeAmount),
fees: {
infrastructureFee: divideByTwo(
@@ -91,5 +92,9 @@ export const useEstimateFees = (
divideByTwo(data.estimateFees.fees.makerFeeVolumeDiscount),
},
}
: data?.estimateFees;
: {
volumeDiscountFactor,
referralDiscountFactor,
...data.estimateFees,
};
};
@@ -3,6 +3,7 @@ import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
import {
calcTradedFactor,
filterAndSortMarkets,
sumFeesFactors,
totalFeesFactorsPercentage,
} from './market-utils';
const { MarketState, MarketTradingMode } = Schema;
@@ -132,3 +133,15 @@ describe('calcTradedFactor', () => {
expect(fa > fb).toBeTruthy();
});
});
describe('sumFeesFactors', () => {
it('does not result in flop errors', () => {
expect(
sumFeesFactors({
makerFee: '0.1',
infrastructureFee: '0.2',
liquidityFee: '0.3',
})
).toEqual(0.6);
});
});
+9 -6
View File
@@ -50,16 +50,19 @@ export const getQuoteName = (market: Partial<Market>) => {
};
export const sumFeesFactors = (fees: Market['fees']['factors']) => {
return fees
? new BigNumber(fees.makerFee)
.plus(fees.liquidityFee)
.plus(fees.infrastructureFee)
: undefined;
if (!fees) return;
return new BigNumber(fees.makerFee)
.plus(fees.liquidityFee)
.plus(fees.infrastructureFee)
.toNumber();
};
export const totalFeesFactorsPercentage = (fees: Market['fees']['factors']) => {
const total = fees && sumFeesFactors(fees);
return total ? formatNumberPercentage(total.times(100)) : undefined;
return total
? formatNumberPercentage(new BigNumber(total).times(100))
: undefined;
};
export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {