Skip to content

Commit

Permalink
fix(trading): fills fees maker discounts (#5406)
Browse files Browse the repository at this point in the history
  • Loading branch information
MadalinaRaicu authored Dec 1, 2023
1 parent 6147122 commit a59f7df
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 241 deletions.
111 changes: 3 additions & 108 deletions libs/fills/src/lib/fills-table.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Trade> = {
Expand Down Expand Up @@ -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: {
Expand All @@ -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
];
Expand Down Expand Up @@ -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()
);
});
});
});
160 changes: 27 additions & 133 deletions libs/fills/src/lib/fills-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -262,73 +254,13 @@ const formatFeeDiscount = (partyId: string) => {
}: VegaValueFormatterParams<Trade, 'market'>) => {
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,
Expand All @@ -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"
>
<p className="mb-1 italic">
{t('If the market was %s', [
Schema.MarketStateMapping[marketState].toLowerCase(),
])}
</p>
{marketState && (
<p className="mb-1 italic">
{t('If the market was %s', [
Schema.MarketStateMapping[marketState].toLowerCase(),
])}
</p>
)}
{role === MAKER && (
<>
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
Expand Down Expand Up @@ -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 (
<div
data-testid="fee-discount-breakdown-tooltip"
Expand Down Expand Up @@ -477,58 +415,14 @@ export const FeesDiscountBreakdownTooltip = ({
label={t('Volume Discount')}
asset={asset}
/>

<dt className="col-span-2">{t('Total Fee Discount')}</dt>
<FeesDiscountBreakdownTooltipItem
value={fees.totalFeeDiscount}
label={''}
asset={asset}
/>
</dl>
</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,
};
};
Loading

0 comments on commit a59f7df

Please sign in to comment.