fix(trading): fills fees maker discounts (#5406)
This commit is contained in:
parent
61471228aa
commit
a59f7dfd29
@ -4,14 +4,8 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import type { Trade } from './fills-data-provider';
|
import type { Trade } from './fills-data-provider';
|
||||||
import {
|
import { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table';
|
||||||
FeesDiscountBreakdownTooltip,
|
|
||||||
FillsTable,
|
|
||||||
getFeesBreakdown,
|
|
||||||
getTotalFeesDiscounts,
|
|
||||||
} from './fills-table';
|
|
||||||
import { generateFill } from './test-helpers';
|
import { generateFill } from './test-helpers';
|
||||||
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
|
|
||||||
|
|
||||||
const partyId = 'party-id';
|
const partyId = 'party-id';
|
||||||
const defaultFill: PartialDeep<Trade> = {
|
const defaultFill: PartialDeep<Trade> = {
|
||||||
@ -66,7 +60,7 @@ describe('FillsTable', () => {
|
|||||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
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({
|
const buyerFill = generateFill({
|
||||||
...defaultFill,
|
...defaultFill,
|
||||||
buyer: {
|
buyer: {
|
||||||
@ -90,7 +84,7 @@ describe('FillsTable', () => {
|
|||||||
'3.00 BTC',
|
'3.00 BTC',
|
||||||
'Maker',
|
'Maker',
|
||||||
'2.00 BTC',
|
'2.00 BTC',
|
||||||
'0.27 BTC',
|
'0.09 BTC',
|
||||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||||
'', // action column
|
'', // 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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -30,19 +30,11 @@ import type {
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import type { Trade } from './fills-data-provider';
|
import type { Trade } from './fills-data-provider';
|
||||||
import type {
|
|
||||||
FillFieldsFragment,
|
|
||||||
TradeFeeFieldsFragment,
|
|
||||||
} from './__generated__/Fills';
|
|
||||||
import { FillActionsDropdown } from './fill-actions-dropdown';
|
import { FillActionsDropdown } from './fill-actions-dropdown';
|
||||||
import { getAsset } from '@vegaprotocol/markets';
|
import { getAsset } from '@vegaprotocol/markets';
|
||||||
|
import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils';
|
||||||
|
|
||||||
const TAKER = 'Taker';
|
type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||||
const MAKER = 'Maker';
|
|
||||||
|
|
||||||
export type Role = typeof TAKER | typeof MAKER | '-';
|
|
||||||
|
|
||||||
export type Props = (AgGridReactProps | AgReactUiProps) & {
|
|
||||||
partyId: string;
|
partyId: string;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
};
|
};
|
||||||
@ -262,73 +254,13 @@ const formatFeeDiscount = (partyId: string) => {
|
|||||||
}: VegaValueFormatterParams<Trade, 'market'>) => {
|
}: VegaValueFormatterParams<Trade, 'market'>) => {
|
||||||
if (!market || !data) return '-';
|
if (!market || !data) return '-';
|
||||||
const asset = getAsset(market);
|
const asset = getAsset(market);
|
||||||
const { fees } = getRoleAndFees({ data, partyId });
|
const { fees: roleFees, role } = getRoleAndFees({ data, partyId });
|
||||||
if (!fees) return '-';
|
if (!roleFees) return '-';
|
||||||
|
const { totalFeeDiscount } = getFeesBreakdown(role, roleFees);
|
||||||
const total = getTotalFeesDiscounts(fees);
|
return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals);
|
||||||
return addDecimalsFormatNumber(total, 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 = ({
|
const FeesBreakdownTooltip = ({
|
||||||
data,
|
data,
|
||||||
value: market,
|
value: market,
|
||||||
@ -350,11 +282,13 @@ const FeesBreakdownTooltip = ({
|
|||||||
data-testid="fee-breakdown-tooltip"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<p className="mb-1 italic">
|
{marketState && (
|
||||||
{t('If the market was %s', [
|
<p className="mb-1 italic">
|
||||||
Schema.MarketStateMapping[marketState].toLowerCase(),
|
{t('If the market was %s', [
|
||||||
])}
|
Schema.MarketStateMapping[marketState].toLowerCase(),
|
||||||
</p>
|
])}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{role === MAKER && (
|
{role === MAKER && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
|
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
|
||||||
@ -425,9 +359,13 @@ export const FeesDiscountBreakdownTooltip = ({
|
|||||||
}
|
}
|
||||||
const asset = getAsset(data.market);
|
const asset = getAsset(data.market);
|
||||||
|
|
||||||
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
|
const {
|
||||||
if (!fees) return null;
|
fees: roleFees,
|
||||||
|
marketState,
|
||||||
|
role,
|
||||||
|
} = getRoleAndFees({ data, partyId }) ?? {};
|
||||||
|
if (!roleFees) return null;
|
||||||
|
const fees = getFeesBreakdown(role, roleFees, marketState);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="fee-discount-breakdown-tooltip"
|
data-testid="fee-discount-breakdown-tooltip"
|
||||||
@ -477,58 +415,14 @@ export const FeesDiscountBreakdownTooltip = ({
|
|||||||
label={t('Volume Discount')}
|
label={t('Volume Discount')}
|
||||||
asset={asset}
|
asset={asset}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<dt className="col-span-2">{t('Total Fee Discount')}</dt>
|
||||||
|
<FeesDiscountBreakdownTooltipItem
|
||||||
|
value={fees.totalFeeDiscount}
|
||||||
|
label={''}
|
||||||
|
asset={asset}
|
||||||
|
/>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
183
libs/fills/src/lib/fills-utils.spec.ts
Normal file
183
libs/fills/src/lib/fills-utils.spec.ts
Normal file
@ -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());
|
||||||
|
});
|
||||||
|
});
|
164
libs/fills/src/lib/fills-utils.ts
Normal file
164
libs/fills/src/lib/fills-utils.ts
Normal file
@ -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'
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user