feat(deal-ticket): enhance for fee discounts (#5353)

This commit is contained in:
Bartłomiej Głownia 2023-11-29 13:59:42 +01:00 committed by GitHub
parent 7588d0cd11
commit a2b9b0da05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 108 additions and 99 deletions

View File

@ -77,12 +77,10 @@ export const DealTicketFeeDetails = ({
label={ label={
<> <>
{t('Fees')} {t('Fees')}
{totalDiscountFactor ? ( {totalDiscountFactor !== '0' ? (
<Pill size="xxs" intent={Intent.Info} className="ml-1"> <Pill size="xxs" intent={Intent.Info} className="ml-1">
-
{formatNumberPercentage( {formatNumberPercentage(
new BigNumber(totalDiscountFactor).multipliedBy(100), new BigNumber(totalDiscountFactor).multipliedBy(100)
2
)} )}
</Pill> </Pill>
) : null} ) : null}
@ -105,10 +103,7 @@ export const DealTicketFeeDetails = ({
)} )}
</p> </p>
<FeesBreakdown <FeesBreakdown
totalFeeAmount={feeEstimate?.totalFeeAmount} feeEstimate={feeEstimate}
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
fees={feeEstimate?.fees}
feeFactors={market.fees.factors} feeFactors={market.fees.factors}
symbol={assetSymbol} symbol={assetSymbol}
decimals={assetDecimals} decimals={assetDecimals}

View File

@ -6,16 +6,19 @@ describe('getDiscountedFee', () => {
discountedFee: '100', discountedFee: '100',
volumeDiscount: '0', volumeDiscount: '0',
referralDiscount: '0', referralDiscount: '0',
totalDiscount: '0',
}); });
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({ expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
discountedFee: '90', discountedFee: '90',
volumeDiscount: '10', volumeDiscount: '10',
referralDiscount: '0', referralDiscount: '0',
totalDiscount: '10',
}); });
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({ expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
discountedFee: '90', discountedFee: '90',
volumeDiscount: '0', volumeDiscount: '0',
referralDiscount: '10', referralDiscount: '10',
totalDiscount: '10',
}); });
}); });
@ -24,6 +27,7 @@ describe('getDiscountedFee', () => {
discountedFee: '', discountedFee: '',
volumeDiscount: '0', volumeDiscount: '0',
referralDiscount: '0', referralDiscount: '0',
totalDiscount: '0',
}); });
}); });
}); });
@ -35,7 +39,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0', volumeDiscountFactor: '0',
referralDiscountFactor: '0', referralDiscountFactor: '0',
}) })
).toEqual(0); ).toEqual('0');
}); });
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => { it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
@ -44,7 +48,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.1', volumeDiscountFactor: '0.1',
referralDiscountFactor: '0', referralDiscountFactor: '0',
}) })
).toEqual(0.1); ).toEqual('-0.1');
}); });
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => { it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
expect( expect(
@ -52,7 +56,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0', volumeDiscountFactor: '0',
referralDiscountFactor: '0.1', referralDiscountFactor: '0.1',
}) })
).toEqual(0.1); ).toEqual('-0.1');
}); });
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => { it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
@ -61,6 +65,6 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.2', volumeDiscountFactor: '0.2',
referralDiscountFactor: '0.1', referralDiscountFactor: '0.1',
}) })
).toBeCloseTo(0.28); ).toBe('-0.28');
}); });
}); });

View File

@ -15,6 +15,7 @@ export const getDiscountedFee = (
discountedFee: feeAmount, discountedFee: feeAmount,
volumeDiscount: '0', volumeDiscount: '0',
referralDiscount: '0', referralDiscount: '0',
totalDiscount: '0',
}; };
} }
const referralDiscount = new BigNumber(referralDiscountFactor || '0') const referralDiscount = new BigNumber(referralDiscountFactor || '0')
@ -23,12 +24,14 @@ export const getDiscountedFee = (
const volumeDiscount = new BigNumber(volumeDiscountFactor || '0') const volumeDiscount = new BigNumber(volumeDiscountFactor || '0')
.multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString()) .multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString())
.toFixed(0, BigNumber.ROUND_FLOOR); .toFixed(0, BigNumber.ROUND_FLOOR);
const totalDiscount = (
BigInt(referralDiscount) + BigInt(volumeDiscount)
).toString();
const discountedFee = ( const discountedFee = (
BigInt(feeAmount || '0') - BigInt(feeAmount || '0') - BigInt(totalDiscount)
BigInt(referralDiscount) -
BigInt(volumeDiscount)
).toString(); ).toString();
return { return {
totalDiscount,
referralDiscount, referralDiscount,
volumeDiscount, volumeDiscount,
discountedFee, discountedFee,
@ -39,16 +42,28 @@ export const getTotalDiscountFactor = (feeEstimate?: {
volumeDiscountFactor?: string; volumeDiscountFactor?: string;
referralDiscountFactor?: string; referralDiscountFactor?: string;
}) => { }) => {
if (!feeEstimate) { if (
return 0; !feeEstimate ||
(feeEstimate.referralDiscountFactor === '0' &&
feeEstimate.volumeDiscountFactor === '0')
) {
return '0';
} }
const volumeFactor = Number(feeEstimate?.volumeDiscountFactor) || 0; const volumeFactor = new BigNumber(
const referralFactor = Number(feeEstimate?.referralDiscountFactor) || 0; feeEstimate?.volumeDiscountFactor || 0
if (!volumeFactor) { ).minus(1);
return referralFactor; const referralFactor = new BigNumber(
feeEstimate?.referralDiscountFactor || 0
).minus(1);
if (volumeFactor.isZero()) {
return feeEstimate.referralDiscountFactor
? `-${feeEstimate.referralDiscountFactor}`
: '0';
} }
if (!referralFactor) { if (referralFactor.isZero()) {
return volumeFactor; return feeEstimate.volumeDiscountFactor
? `-${feeEstimate.volumeDiscountFactor}`
: '0';
} }
return 1 - (1 - volumeFactor) * (1 - referralFactor); return volumeFactor.multipliedBy(referralFactor).minus(1).toString();
}; };

View File

@ -14,13 +14,16 @@ describe('FeesBreakdown', () => {
liquidityFee: '100', liquidityFee: '100',
}; };
const props = { const props = {
totalFeeAmount: '100',
fees,
feeFactors, feeFactors,
symbol: 'USD', symbol: 'USD',
decimals: 2, decimals: 2,
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01', feeEstimate: {
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01',
totalFeeAmount: '100',
fees,
},
}; };
render(<FeesBreakdown {...props} />); render(<FeesBreakdown {...props} />);
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent( expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(

View File

@ -1,12 +1,13 @@
import { sumFeesFactors } from '@vegaprotocol/markets'; import { sumFeesFactors } from '@vegaprotocol/markets';
import type { TradeFee, FeeFactors } from '@vegaprotocol/types'; import type { FeeFactors } from '@vegaprotocol/types';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
formatNumberPercentage, formatNumberPercentage,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { getDiscountedFee } from '../discounts'; import { getDiscountedFee, getTotalDiscountFactor } from '../discounts';
import { useT } from '../../use-t'; import { useT } from '../../use-t';
import { type useEstimateFees } from '../../hooks/use-estimate-fees';
const formatValue = ( const formatValue = (
value: string | number | null | undefined, value: string | number | null | undefined,
@ -24,18 +25,16 @@ const FeesBreakdownItem = ({
decimals, decimals,
}: { }: {
label: string; label: string;
factor?: string; factor?: string | number;
value: string; value: string;
symbol?: string; symbol?: string;
decimals: number; decimals: number;
}) => ( }) => (
<> <>
<dt className="col-span-2">{label}</dt> <dt className="col-span-2">{label}</dt>
{factor && ( <dd className="text-right col-span-1">
<dd className="text-right col-span-1"> {factor ? formatNumberPercentage(new BigNumber(factor).times(100)) : ''}
{formatNumberPercentage(new BigNumber(factor).times(100))} </dd>
</dd>
)}
<dd className="text-right col-span-3"> <dd className="text-right col-span-3">
{formatValue(value, decimals)} {symbol || ''} {formatValue(value, decimals)} {symbol || ''}
</dd> </dd>
@ -43,59 +42,35 @@ const FeesBreakdownItem = ({
); );
export const FeesBreakdown = ({ export const FeesBreakdown = ({
totalFeeAmount, feeEstimate,
fees,
feeFactors, feeFactors,
symbol, symbol,
decimals, decimals,
referralDiscountFactor,
volumeDiscountFactor,
}: { }: {
totalFeeAmount?: string; feeEstimate: ReturnType<typeof useEstimateFees>;
fees?: TradeFee;
feeFactors?: FeeFactors; feeFactors?: FeeFactors;
symbol?: string; symbol?: string;
decimals: number; decimals: number;
referralDiscountFactor?: string;
volumeDiscountFactor?: string;
}) => { }) => {
const t = useT(); const t = useT();
const { fees, totalFeeAmount, referralDiscountFactor, volumeDiscountFactor } =
feeEstimate || {};
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null; if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee( const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
fees.infrastructureFee, const { discountedFee: discountedTotalFeeAmount, totalDiscount } =
referralDiscountFactor, getDiscountedFee(
volumeDiscountFactor totalFeeAmount,
); referralDiscountFactor,
volumeDiscountFactor
const { discountedFee: discountedLiquidityFee } = getDiscountedFee( );
fees.liquidityFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { discountedFee: discountedMakerFee } = getDiscountedFee(
fees.makerFee,
referralDiscountFactor,
volumeDiscountFactor
);
const {
discountedFee: discountedTotalFeeAmount,
volumeDiscount,
referralDiscount,
} = getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
);
return ( return (
<dl className="grid grid-cols-6"> <dl className="grid grid-cols-6">
<FeesBreakdownItem <FeesBreakdownItem
label={t('Infrastructure fee')} label={t('Infrastructure fee')}
factor={feeFactors?.infrastructureFee} factor={feeFactors?.infrastructureFee}
value={discountedInfrastructureFee} value={fees.infrastructureFee}
symbol={symbol} symbol={symbol}
decimals={decimals} decimals={decimals}
/> />
@ -103,7 +78,7 @@ export const FeesBreakdown = ({
<FeesBreakdownItem <FeesBreakdownItem
label={t('Liquidity fee')} label={t('Liquidity fee')}
factor={feeFactors?.liquidityFee} factor={feeFactors?.liquidityFee}
value={discountedLiquidityFee} value={fees.liquidityFee}
symbol={symbol} symbol={symbol}
decimals={decimals} decimals={decimals}
/> />
@ -111,35 +86,50 @@ export const FeesBreakdown = ({
<FeesBreakdownItem <FeesBreakdownItem
label={t('Maker fee')} label={t('Maker fee')}
factor={feeFactors?.makerFee} factor={feeFactors?.makerFee}
value={discountedMakerFee} value={fees.makerFee}
symbol={symbol} symbol={symbol}
decimals={decimals} decimals={decimals}
/> />
{volumeDiscountFactor && volumeDiscount !== '0' && ( {totalDiscount && totalDiscount !== '0' ? (
<FeesBreakdownItem <>
label={t('Volume discount')} <FeesBreakdownItem
factor={volumeDiscountFactor} label={t('Subtotal')}
value={volumeDiscount} value={totalFeeAmount}
symbol={symbol} factor={
decimals={decimals} feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
/> }
symbol={symbol}
decimals={decimals}
/>
<div className="col-span-6 mt-2"></div>
<FeesBreakdownItem
label={t('Discount')}
factor={totalDiscountFactor}
value={`-${totalDiscount}`}
symbol={symbol}
decimals={decimals}
/>
<FeesBreakdownItem
label={t('Total')}
value={discountedTotalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</>
) : (
<>
<div className="col-span-6 mt-2"></div>
<FeesBreakdownItem
label={t('Total')}
factor={
feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
}
value={totalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</>
)} )}
{referralDiscountFactor && referralDiscount !== '0' && (
<FeesBreakdownItem
label={t('Referral discount')}
factor={referralDiscountFactor}
value={referralDiscount}
symbol={symbol}
decimals={decimals}
/>
)}
<FeesBreakdownItem
label={t('Total fees')}
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
value={discountedTotalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</dl> </dl>
); );
}; };

View File

@ -21,6 +21,7 @@
"DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT": "To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.", "DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT": "To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.",
"Deposit {{assetSymbol}}": "Deposit {{assetSymbol}}", "Deposit {{assetSymbol}}": "Deposit {{assetSymbol}}",
"Devnet": "Devnet", "Devnet": "Devnet",
"Discount": "Discount",
"EST_TOTAL_MARGIN_TOOLTIP_TEXT": "Estimated total margin that will cover open positions, active orders and this order.", "EST_TOTAL_MARGIN_TOOLTIP_TEXT": "Estimated total margin that will cover open positions, active orders and this order.",
"Est. uncrossing price": "Est. uncrossing price", "Est. uncrossing price": "Est. uncrossing price",
"Est. uncrossing vol": "Est. uncrossing vol", "Est. uncrossing vol": "Est. uncrossing vol",
@ -78,6 +79,7 @@
"Size": "Size", "Size": "Size",
"Size cannot be lower than {{sizeStep}}": "Size cannot be lower than {{sizeStep}}", "Size cannot be lower than {{sizeStep}}": "Size cannot be lower than {{sizeStep}}",
"sizeAtPrice-market": "market", "sizeAtPrice-market": "market",
"Subtotal": "Subtotal",
"Stagnet": "Stagnet", "Stagnet": "Stagnet",
"Stop": "Stop", "Stop": "Stop",
"Stop Limit": "Stop Limit", "Stop Limit": "Stop Limit",
@ -104,7 +106,7 @@
"Time in force": "Time in force", "Time in force": "Time in force",
"TIME_IN_FORCE_SELECTOR_LIQUIDITY_MONITORING_AUCTION": "This market is in auction until it reaches <0>sufficient liquidity</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders.", "TIME_IN_FORCE_SELECTOR_LIQUIDITY_MONITORING_AUCTION": "This market is in auction until it reaches <0>sufficient liquidity</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders.",
"TIME_IN_FORCE_SELECTOR_PRICE_MONITORING_AUCTION": "This market is in auction due to <0>high price volatility</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders.", "TIME_IN_FORCE_SELECTOR_PRICE_MONITORING_AUCTION": "This market is in auction due to <0>high price volatility</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders.",
"Total fees": "Total fees", "Total": "Total",
"Total margin available": "Total margin available", "Total margin available": "Total margin available",
"TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).", "TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).",
"Trading terminated": "Trading terminated", "Trading terminated": "Trading terminated",