Compare commits

...
8 changed files with 303 additions and 154 deletions
@@ -7,7 +7,7 @@ import {
NetworkParams,
} from '@vegaprotocol/network-parameters';
import { useMarketList } from '@vegaprotocol/markets';
import { formatNumber } from '@vegaprotocol/utils';
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
import { FeeCard } from './fees-card';
import { MarketFees } from './market-fees';
@@ -284,7 +284,7 @@ export const CurrentVolume = ({
return (
<div>
<Stat
value={formatNumber(windowLengthVolume)}
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
text={t('Past %s epochs', epochs.toString())}
/>
{requiredForNextTier > 0 && (
@@ -5,6 +5,8 @@ import { t } from '@vegaprotocol/i18n';
import { formatPercentage, getAdjustedFee } from './utils';
import { MarketCodeCell } from '../../client-pages/markets/market-code-cell';
import BigNumber from 'bignumber.js';
import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
import { Links } from '../../lib/links';
const feesTableColumnDefs = [
{ field: 'code', cellRenderer: 'MarketCodeCell' },
@@ -51,6 +53,8 @@ export const MarketFees = ({
referralDiscount: number;
volumeDiscount: number;
}) => {
const navigateWithMeta = useNavigateWithMeta();
const rows = compact(markets || []).map((m) => {
const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
const makerFee = new BigNumber(m.fees.factors.makerFee);
@@ -63,6 +67,7 @@ export const MarketFees = ({
);
return {
id: m.id,
code: m.tradableInstrument.instrument.code,
productType: m.tradableInstrument.instrument.product.__typename,
infraFee: formatPercentage(infraFee.toNumber()),
@@ -80,10 +85,19 @@ export const MarketFees = ({
<AgGrid
columnDefs={feesTableColumnDefs}
rowData={rows}
getRowId={({ data }) => data.id}
defaultColDef={feesTableDefaultColDef}
domLayout="autoHeight"
components={components}
rowHeight={45}
rowClass="cursor-pointer"
onRowClicked={({ data, event }) => {
navigateWithMeta(
Links.MARKET(data.id),
// @ts-ignore metaKey and ctrlKey exist
event.metaKey || event.ctrlKey
);
}}
/>
</div>
);
@@ -0,0 +1,74 @@
import BigNumber from 'bignumber.js';
import { getAdjustedFee } from './utils';
describe('getAdjustedFee', () => {
it('simple', () => {
const volumeDiscount = 0.5;
const referralDiscount = 0.5;
const infraFee = 0.1;
const makerFee = 0.1;
const liqFee = 0.1;
const fees = [
new BigNumber(infraFee),
new BigNumber(makerFee),
new BigNumber(liqFee),
];
const discounts = [
new BigNumber(volumeDiscount),
new BigNumber(referralDiscount),
];
// 1 - 0.5 - 0.5
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
// 1 - 0.5 = 0.5
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
// 0.5 * 0.5 = 0.25
// 1 - 0.25 = 0.75
const factor = new BigNumber(1).minus(v.times(r));
// 0.1 + 0.1 + 0.1 = 0.3
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
// 0.3 * 0.75 = 0.225
const expected = new BigNumber(totalFees).times(factor).toNumber();
expect(getAdjustedFee(fees, discounts)).toBe(expected);
});
it('combines discount factors multiplicativly', () => {
const volumeDiscount = 0.4;
const referralDiscount = 0.1;
const infraFee = 0.0005;
const makerFee = 0.0002;
const liqFee = 0.01;
const fees = [
new BigNumber(infraFee),
new BigNumber(makerFee),
new BigNumber(liqFee),
];
const discounts = [
new BigNumber(volumeDiscount),
new BigNumber(referralDiscount),
];
// formula for calculating adjusted fees
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
const factor = new BigNumber(1).minus(v.times(r));
// summed fees
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
const expected = new BigNumber(totalFees).times(factor).toNumber();
expect(getAdjustedFee(fees, discounts)).toBe(expected);
});
});
@@ -88,14 +88,18 @@ export const getReferralBenefitTier = (
/**
* Given a set of fees and a set of discounts return
* the adjusted fee factor
*
* Formula for calculating the adjusted fees
* total_discount_factor = 1 - (1 - volumeDiscount) * (1 - referralDiscount)
*/
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
const totalDiscount = discounts.reduce(
(sum, d) => sum.plus(d),
new BigNumber(0)
);
return totalFee
.times(BigNumber.max(0, new BigNumber(1).minus(totalDiscount)))
.toNumber();
const combinedFactors = discounts.reduce((acc, d) => {
return acc.times(new BigNumber(1).minus(d));
}, new BigNumber(1));
const totalFactor = new BigNumber(1).minus(combinedFactors);
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
};
@@ -1,77 +1,68 @@
import { render, screen } from '@testing-library/react';
import { Sparkline } from './sparkline';
import type { SparklineProps } from './sparkline';
const props = {
data: [
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
10, 11, 12,
],
muted: true,
};
describe('Sparkline', () => {
let props: SparklineProps = {
data: [],
};
it('Renders an svg with a single path', () => {
render(<Sparkline {...props} />);
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toBeInTheDocument();
expect(path).toHaveAttribute('d', expect.any(String));
expect(path).toHaveAttribute('stroke', expect.any(String));
expect(path).toHaveAttribute('stroke-width', '1');
expect(path).toHaveAttribute('fill', 'transparent');
});
beforeEach(() => {
props = {
data: [
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
9, 10, 11, 12,
],
};
});
it('Requires a data prop but width and height are optional', () => {
render(<Sparkline {...props} />);
const svg = screen.getByTestId('sparkline-svg');
expect(svg).toHaveAttribute('width', '60');
expect(svg).toHaveAttribute('height', '15');
});
it('Renders an svg with a single path', () => {
render(<Sparkline {...props} />);
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toBeInTheDocument();
expect(path).toHaveAttribute('d', expect.any(String));
expect(path).toHaveAttribute('stroke-width', '1');
});
it('Renders a red line if the last value is less than the first', () => {
props.data[0] = 10;
props.data[props.data.length - 1] = 5;
render(<Sparkline {...props} />);
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toHaveClass(
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
);
});
it('Requires a data prop but width and height are optional', () => {
render(<Sparkline {...props} />);
const svg = screen.getByTestId('sparkline-svg');
expect(svg).toHaveAttribute('width', '60');
expect(svg).toHaveAttribute('height', '15');
});
it('Renders a green line if the last value is greater than the first', () => {
props.data[0] = 5;
props.data[props.data.length - 1] = 10;
props.muted = true;
render(<Sparkline {...props} />);
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toHaveClass(
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
);
});
it('Renders a red line if the last value is less than the first', () => {
props.data[0] = 10;
props.data[props.data.length - 1] = 5;
render(<Sparkline {...props} />);
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toHaveClass(
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
);
});
it('Renders a white line if the first and last values are equal', () => {
props.data[0] = 5;
props.data[props.data.length - 1] = 5;
render(<Sparkline {...props} />);
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toHaveClass(
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
);
});
it('Renders a green line if the last value is greater than the first', () => {
props.data[0] = 5;
props.data[props.data.length - 1] = 10;
render(<Sparkline {...props} />);
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toHaveClass(
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
);
});
it('Renders a gray line if there are not 24 values', () => {
props.data = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23,
];
render(<Sparkline {...props} />);
const paths = screen.queryAllByTestId('sparkline-path');
expect(paths).toHaveLength(2);
expect(paths[0]).toHaveClass(
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
);
it('Renders a white line if the first and last values are equal', () => {
props.data[0] = 5;
props.data[props.data.length - 1] = 5;
render(<Sparkline {...props} />);
const paths = screen.getAllByTestId('sparkline-path');
const path = paths[0];
expect(path).toHaveClass(
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
);
});
});
@@ -8,39 +8,49 @@ export default {
const Template: Story = (args) => <Sparkline data={args['data']} {...args} />;
export const Grey = Template.bind({});
Grey.args = {
data: [
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
],
width: 60,
height: 30,
points: 25,
className: 'w-[113px]',
};
export const Equal = Template.bind({});
Equal.args = {
data: [
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
10, 11, 12,
],
width: 60,
width: 110,
height: 30,
points: 25,
className: 'w-[113px]',
};
export const Increase = Template.bind({});
Increase.args = {
data: [
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
10, 11, 12,
22,
22,
22, // extra values should be ignored, this should still render an increase
0,
2,
3,
4,
5,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
6,
7,
8,
9,
10,
11,
12,
],
width: 60,
width: 110,
height: 30,
points: 25,
className: 'w-[113px]',
};
export const Decrease = Template.bind({});
@@ -49,8 +59,27 @@ Decrease.args = {
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
10, 11, 1,
],
width: 60,
width: 110,
height: 30,
};
export const LessThan24HoursIncrease = Template.bind({});
LessThan24HoursIncrease.args = {
data: [20, 21, 22, 25, 24, 24, 22, 19, 20, 22, 23, 27],
width: 110,
height: 30,
};
export const LessThan24HoursDecrease = Template.bind({});
LessThan24HoursDecrease.args = {
data: [20, 21, 22, 23, 24, 6, 7, 9, 11, 13, 11, 9],
width: 110,
height: 30,
};
export const NoData = Template.bind({});
NoData.args = {
data: [],
width: 110,
height: 30,
points: 25,
className: 'w-[113px]',
};
@@ -1,15 +1,25 @@
import { extent } from 'd3-array';
import { scaleLinear } from 'd3-scale';
import { line } from 'd3-shape';
import { area, line } from 'd3-shape';
import isEqual from 'lodash/isEqual';
import React from 'react';
function colorByChange(a: number, b: number) {
return a === b
? 'stroke-black/40 dark:stroke-white/40'
: a < b
? 'stroke-market-green-600 dark:stroke-market-green'
: 'stroke-market-red dark:stroke-market-red';
if (a < b) {
return 'stroke-market-green-600 dark:stroke-market-green';
} else if (a > b) {
return 'stroke-market-red dark:stroke-market-red';
}
return 'stroke-black/40 dark:stroke-white/40';
}
function shadedColor(a: number, b: number) {
if (a < b) {
return 'fill-market-green-600';
} else if (a > b) {
return 'fill-market-red';
}
return 'fill-black dark:fill-white';
}
export interface SparklineProps {
@@ -18,75 +28,60 @@ export interface SparklineProps {
height?: number;
points?: number;
className?: string;
muted?: boolean;
}
export const SparklineView = ({
data,
width = 60,
height = 15,
points = 25,
muted = false,
points = 24,
className,
}: SparklineProps) => {
// How many points are missing. If market is 12 hours old the 25 - 12
const preMarketLength = points - data.length;
// Create two dimensional array for sparkline points [x, y]
const marketData: [number, number][] = data.map((d, i) => [
preMarketLength + i,
d,
]);
// Empty two dimensional array for gray, 'no data' line
let preMarketData: [number, number][] = [];
// Get the extent for our y value
const [min, max] = extent(marketData, (d) => d[1]);
const [min, max] = extent(data, (d) => d);
if (typeof min !== 'number' || typeof max !== 'number') {
return null;
}
// Create a second set of data to render a gray line for any
// missing points if the market is less than 24 hours old
if (marketData.length < points) {
// Populate preMarketData with the average of our extents
// so that the line renders centered vertically
const fillValue = (min + max) / 2;
preMarketData = new Array(points - marketData.length)
.fill(fillValue)
.map((d: number, i) => [i, d] as [number, number]);
const midValue = (min + max) / 2;
// Add the first point of or market data so that the two
// lines join up
preMarketData.push(marketData[0] as [number, number]);
}
// Market may be less than 24hr old so padd the data array
// with values that is the mid value (avg of min and max).
// This will rendera horizontal line until the real data shifts the line
const padCount = data.length < points ? points - data.length : 0;
const padArr = new Array(padCount).fill(midValue);
const trimmedData = data.slice(-points);
const xScale = scaleLinear().domain([0, points]).range([0, 100]);
const yScale = scaleLinear().domain([min, max]).range([100, 0]);
// Get the last 24 values if data has more than needed
const lineData: [number, number][] = [...padArr, ...trimmedData].map(
(d, i) => {
return [i, d];
}
);
const xScale = scaleLinear().domain([0, points]).range([0, width]);
const yScale = scaleLinear().domain([min, max]).range([height, 0]);
const lineSeries = line()
.x((d) => xScale(d[0]))
.y((d) => yScale(d[1]));
// Get the color of the marketData line
const [firstVal, lastVal] = [data[0], data[data.length - 1]];
const strokeClassName = muted
? data.length >= 24
? colorByChange(firstVal, lastVal)
: 'stroke-black/40 dark:stroke-white/40'
: colorByChange(firstVal, lastVal);
const areaSeries = area()
.x((d) => xScale(d[0]))
.y0(height)
.y1((d) => yScale(d[1]));
const firstVal = trimmedData[0];
const lastVal = trimmedData[trimmedData.length - 1];
// Get the color of the marketData line depending on market movement
const strokeClassName = colorByChange(firstVal, lastVal);
const areaClassName = shadedColor(firstVal, lastVal);
// Create paths
const preMarketCreationPath = lineSeries(preMarketData);
const mainPath = lineSeries(marketData);
const pathProps = {
'data-testid': 'sparkline-path',
className: `[vector-effect:non-scaling-stroke] ${strokeClassName}`,
stroke: 'strokeCurrent',
strokeWidth: 1,
fill: 'transparent',
};
const linePath = lineSeries(lineData);
const areaPath = areaSeries(lineData);
return (
<svg
@@ -94,13 +89,25 @@ export const SparklineView = ({
className={className}
width={width}
height={height}
viewBox="0 0 100 100"
viewBox={`0 0 ${width} ${height}`}
preserveAspectRatio="none"
>
{preMarketCreationPath && (
<path {...pathProps} d={preMarketCreationPath} />
{linePath && (
<path
d={linePath}
data-testid="sparkline-path"
className={`[vector-effect:non-scaling-stroke] fill-transparent ${strokeClassName}`}
strokeWidth={1}
/>
)}
{areaPath && (
<path
className={areaClassName}
fillOpacity={0.2}
stroke="none"
d={areaPath}
/>
)}
{mainPath && <path {...pathProps} d={mainPath} />}
</svg>
);
};
+30
View File
@@ -179,3 +179,33 @@ export const toNumberParts = (
export const isNumeric = (
value?: string | number | BigNumber | bigint | null
): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value));
/**
* Format a number greater than 1 million with m for million, b for billion
* and t for trillion
*/
export const formatNumberRounded = (num: BigNumber) => {
let value = '';
const format = (divisor: string) => {
const result = num.dividedBy(divisor);
return result.isInteger() ? result.toString() : result.toFixed(1);
};
if (num.isGreaterThan(new BigNumber('1e14'))) {
value = '>100t';
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
// Trillion
value = `${format('1e12')}t`;
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
// Billion
value = `${format('1e9')}b`;
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
// Million
value = `${format('1e6')}m`;
} else {
value = formatNumber(num);
}
return value;
};