feat: update resolution creation to add more aggregation levels

This commit is contained in:
Matthew Russell
2023-09-01 09:28:17 -07:00
parent e22ec17892
commit 174beee7dc
3 changed files with 131 additions and 44 deletions
@@ -7,7 +7,7 @@ import {
TradingDropdownContent,
TradingDropdownItem,
} from '@vegaprotocol/ui-toolkit';
import { formatNumberFixed } from '@vegaprotocol/utils';
import { addDecimal, getNumberFormat } from '@vegaprotocol/utils';
export const OrderbookControls = ({
lastTradedPrice,
@@ -15,27 +15,14 @@ export const OrderbookControls = ({
decimalPlaces,
setResolution,
}: {
lastTradedPrice: string | undefined;
lastTradedPrice: string;
resolution: number;
decimalPlaces: number;
setResolution: (resolution: number) => void;
}) => {
const [isOpen, setOpen] = useState(false);
const resolutions = new Array(
Math.max(lastTradedPrice?.toString().length ?? 0, decimalPlaces + 1)
)
.fill(null)
.map((v, i) => Math.pow(10, i));
const formatResolution = (r: number) => {
return formatNumberFixed(
Math.log10(r) - decimalPlaces > 0
? Math.pow(10, Math.log10(r) - decimalPlaces)
: 0,
decimalPlaces - Math.log10(r)
);
};
const resolutions = createResolutions(lastTradedPrice, decimalPlaces);
const increaseResolution = () => {
const index = resolutions.indexOf(resolution);
@@ -56,7 +43,7 @@ export const OrderbookControls = ({
<button
onClick={increaseResolution}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
className="flex items-center px-2 border-r cursor-pointer border-default"
className="flex items-center px-2 border-r cursor-pointer border-default disabled:cursor-default"
data-testid="plus-button"
>
<VegaIcon size={12} name={VegaIconNames.PLUS} />
@@ -67,12 +54,14 @@ export const OrderbookControls = ({
trigger={
<TradingDropdownTrigger data-testid="resolution">
<button
className="flex items-center px-2 text-left gap-1"
className="flex items-center justify-between px-2 gap-1"
style={{
minWidth: `${
Math.max.apply(
null,
resolutions.map((item) => formatResolution(item).length)
resolutions.map(
(item) => formatResolution(item, decimalPlaces).length
)
) + 5
}ch`,
}}
@@ -83,15 +72,19 @@ export const OrderbookControls = ({
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
{formatResolution(resolution)}
{formatResolution(resolution, decimalPlaces)}
</button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align="start">
{resolutions.map((r) => (
<TradingDropdownItem key={r} onClick={() => setResolution(r)}>
{formatResolution(r)}
<TradingDropdownItem
key={r}
onClick={() => setResolution(r)}
className="justify-end"
>
{formatResolution(r, decimalPlaces)}
</TradingDropdownItem>
))}
</TradingDropdownContent>
@@ -99,7 +92,7 @@ export const OrderbookControls = ({
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center px-2 cursor-pointer border-x border-default"
className="flex items-center px-2 cursor-pointer border-x border-default disabled:cursor-default"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
@@ -107,3 +100,47 @@ export const OrderbookControls = ({
</div>
);
};
export const formatResolution = (r: number, decimalPlaces: number) => {
const num = addDecimal(r, decimalPlaces);
// Wrap with Number to trim trailing 0s
const x = Number(num);
if (x >= 1) {
return getNumberFormat(0).format(x);
}
return x.toString();
};
export const createResolutions = (
lastTradedPrice: string,
decimalPlaces: number
) => {
// number of levels determined by either the number
// of digits in the last traded price OR the number of decimal
// places. For example:
//
// last traded = 1 (0.001)
// dps = 3
// result = 3
//
// last traded = 100001 (1000.01
// dps = 2
// result = 6
const levelCount = Math.max(lastTradedPrice.length ?? 0, decimalPlaces + 1);
const generatedResolutions = new Array(levelCount)
.fill(null)
.map((_, i) => Math.pow(10, i));
const customResolutions = [2, 5, 20, 50, 200, 500];
const combined = customResolutions.concat(generatedResolutions);
combined.sort((a, b) => a - b);
// Remove any resolutions higher than the generated ones as
// we dont want a custom resolution higher than necessary
const resolutions = combined.filter((r) => {
return r <= generatedResolutions[generatedResolutions.length - 1];
});
return resolutions;
};
+61 -16
View File
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
import { generateMockData, VolumeType } from './orderbook-data';
import { Orderbook, OrderbookMid } from './orderbook';
import * as orderbookData from './orderbook-data';
import { createResolutions } from './orderbook-controls';
function mockOffsetSize(width: number, height: number) {
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
@@ -37,7 +38,8 @@ describe('Orderbook', () => {
jest.clearAllMocks();
mockOffsetSize(800, 768);
});
it('markPrice should be in the middle', async () => {
it('lastTradedPrice should be in the middle', async () => {
render(
<Orderbook
decimalPlaces={decimalPlaces}
@@ -71,6 +73,7 @@ describe('Orderbook', () => {
expect(
await screen.findByTestId(`last-traded-${params.lastTradedPrice}`)
).toBeInTheDocument();
// Before resolution change the price is 122.934
await userEvent.click(screen.getByTestId('price-122901'));
expect(onClickSpy).toBeCalledWith({ price: '122.901' });
@@ -86,15 +89,16 @@ describe('Orderbook', () => {
expect(orderbookData.compactRows).toHaveBeenCalledWith(
mockedData.bids,
VolumeType.bid,
10
2
);
expect(orderbookData.compactRows).toHaveBeenCalledWith(
mockedData.asks,
VolumeType.ask,
10
2
);
await userEvent.click(screen.getByTestId('price-12294'));
expect(onClickSpy).toBeCalledWith({ price: '122.94' });
await userEvent.click(screen.getByTestId('price-122938'));
expect(onClickSpy).toBeCalledWith({ price: '122.938' });
});
it('plus - minus buttons should change resolution', async () => {
@@ -114,26 +118,30 @@ describe('Orderbook', () => {
1
);
expect(screen.getByTestId('minus-button')).toBeDisabled();
userEvent.click(screen.getByTestId('plus-button'));
await userEvent.click(screen.getByTestId('plus-button'));
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
2
);
await userEvent.click(screen.getByTestId('plus-button'));
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
5
);
expect(screen.getByTestId('minus-button')).not.toBeDisabled();
await userEvent.click(screen.getByTestId('minus-button'));
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
10
2
);
});
expect(screen.getByTestId('minus-button')).not.toBeDisabled();
userEvent.click(screen.getByTestId('minus-button'));
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
1
);
});
expect(screen.getByTestId('minus-button')).toBeDisabled();
await userEvent.click(screen.getByTestId('resolution'));
await userEvent.click(screen.getByTestId('resolution'));
await waitFor(() => {
expect(screen.getByRole('menu')).toBeInTheDocument();
});
await userEvent.click(screen.getAllByRole('menuitem')[5]);
await userEvent.click(screen.getAllByRole('menuitem')[11]);
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
100000
@@ -223,3 +231,40 @@ describe('OrderbookMid', () => {
expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument();
});
});
describe('createResolutions', () => {
it('create resolutions relative to the market', () => {
expect(
createResolutions(
'1', // 0.001
3
)
).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]);
expect(
createResolutions(
'190017', // 1900.17
2
)
).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000]);
expect(
createResolutions(
'123456789', // 1234.56789
5
)
).toEqual([
1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000, 1000000,
10000000, 100000000,
]);
});
it('removes resolutions that arent precise enough for the market', () => {
expect(
createResolutions(
'1', // 0.01
2
)
).toEqual([1, 2, 5, 10, 20, 50, 100]);
});
});
+10 -5
View File
@@ -23,6 +23,7 @@ const OrderbookSide = ({
type,
decimalPlaces,
positionDecimalPlaces,
priceFormatDecimalPlaces,
onClick,
width,
maxVol,
@@ -31,16 +32,12 @@ const OrderbookSide = ({
resolution: number;
decimalPlaces: number;
positionDecimalPlaces: number;
priceFormatDecimalPlaces: number;
type: VolumeType;
onClick: (args: { price?: string; size?: string }) => void;
width: number;
maxVol: number;
}) => {
// we'll want to only display a relevant number of dps based on the
// current resolution selection
const priceFormatDecimalPlaces = Math.ceil(
decimalPlaces - Math.log10(resolution)
);
return (
<div
className={
@@ -171,6 +168,12 @@ export const Orderbook = ({
const bestAskPrice = asks[0] ? asks[0].price : '0';
const bestBidPrice = bids[0] ? bids[0].price : '0';
// we'll want to only display a relevant number of dps based on the
// current resolution selection
const priceFormatDecimalPlaces = Math.ceil(
decimalPlaces - Math.log10(resolution)
);
return (
<div className="h-full text-xs grid grid-rows-[1fr_min-content]">
<div>
@@ -209,6 +212,7 @@ export const Orderbook = ({
resolution={resolution}
decimalPlaces={decimalPlaces}
positionDecimalPlaces={positionDecimalPlaces}
priceFormatDecimalPlaces={priceFormatDecimalPlaces}
onClick={onClick}
width={width}
maxVol={maxVol}
@@ -226,6 +230,7 @@ export const Orderbook = ({
resolution={resolution}
decimalPlaces={decimalPlaces}
positionDecimalPlaces={positionDecimalPlaces}
priceFormatDecimalPlaces={priceFormatDecimalPlaces}
onClick={onClick}
width={width}
maxVol={maxVol}