feat: update resolution creation to add more aggregation levels
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user