feat(trading): orderbook changes (#4652)

This commit is contained in:
Matthew Russell 2023-08-31 13:34:13 -07:00 committed by GitHub
parent 255c3752f2
commit 559ef48d6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 434 additions and 341 deletions

View File

@ -6,7 +6,7 @@ const askVolume = 'ask-vol-9894185';
const bidVolume = 'bid-vol-9889001'; const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894185'; const askCumulative = 'cumulative-vol-9894185';
const bidCumulative = 'cumulative-vol-9889001'; const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000'; const midPrice = 'last-traded-4612690000';
const priceResolution = 'resolution'; const priceResolution = 'resolution';
const dealTicketPrice = 'order-price'; const dealTicketPrice = 'order-price';
const dealTicketSize = 'order-size'; const dealTicketSize = 'order-size';

View File

@ -69,6 +69,7 @@ describe('MarketSelectorItem', () => {
targetStake: '1000000', targetStake: '1000000',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED, trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null, priceMonitoringBounds: null,
lastTradedPrice: '100',
}; };
const candles = [ const candles = [

View File

@ -0,0 +1,109 @@
import { useState } from 'react';
import {
VegaIcon,
VegaIconNames,
TradingDropdown,
TradingDropdownTrigger,
TradingDropdownContent,
TradingDropdownItem,
} from '@vegaprotocol/ui-toolkit';
import { formatNumberFixed } from '@vegaprotocol/utils';
export const OrderbookControls = ({
lastTradedPrice,
resolution,
decimalPlaces,
setResolution,
}: {
lastTradedPrice: string | undefined;
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 increaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index < resolutions.length - 1) {
setResolution(resolutions[index + 1]);
}
};
const decreaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index > 0) {
setResolution(resolutions[index - 1]);
}
};
return (
<div className="flex h-6">
<button
onClick={increaseResolution}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
className="flex items-center px-2 border-r cursor-pointer border-default"
data-testid="plus-button"
>
<VegaIcon size={12} name={VegaIconNames.PLUS} />
</button>
<TradingDropdown
open={isOpen}
onOpenChange={(open) => setOpen(open)}
trigger={
<TradingDropdownTrigger data-testid="resolution">
<button
className="flex items-center px-2 text-left gap-1"
style={{
minWidth: `${
Math.max.apply(
null,
resolutions.map((item) => formatResolution(item).length)
) + 5
}ch`,
}}
>
<VegaIcon
size={12}
name={
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
{formatResolution(resolution)}
</button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align="start">
{resolutions.map((r) => (
<TradingDropdownItem key={r} onClick={() => setResolution(r)}>
{formatResolution(r)}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center px-2 cursor-pointer border-x border-default"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
</button>
</div>
);
};

View File

@ -31,21 +31,12 @@ describe('compactRows', () => {
it('counts cumulative vol', () => { it('counts cumulative vol', () => {
const asks = compactRows(sell, VolumeType.ask, 10); const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10); const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol.value).toEqual(4950); expect(asks[0].cumulativeVol).toEqual(4950);
expect(bids[0].cumulativeVol.value).toEqual(579); expect(bids[0].cumulativeVol).toEqual(579);
expect(asks[10].cumulativeVol.value).toEqual(390); expect(asks[10].cumulativeVol).toEqual(390);
expect(bids[10].cumulativeVol.value).toEqual(4950); expect(bids[10].cumulativeVol).toEqual(4950);
expect(bids[bids.length - 1].cumulativeVol.value).toEqual(4950); expect(bids[bids.length - 1].cumulativeVol).toEqual(4950);
expect(asks[asks.length - 1].cumulativeVol.value).toEqual(390); expect(asks[asks.length - 1].cumulativeVol).toEqual(390);
});
it('updates relative data', () => {
const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol.relativeValue).toEqual(100);
expect(bids[0].cumulativeVol.relativeValue).toEqual(12);
expect(asks[10].cumulativeVol.relativeValue).toEqual(8);
expect(bids[10].cumulativeVol.relativeValue).toEqual(100);
}); });
}); });

View File

@ -5,15 +5,11 @@ export enum VolumeType {
bid, bid,
ask, ask,
} }
export interface CumulativeVol {
value: number;
relativeValue?: number;
}
export interface OrderbookRowData { export interface OrderbookRowData {
price: string; price: string;
value: number; volume: number;
cumulativeVol: CumulativeVol; cumulativeVol: number;
} }
export const getPriceLevel = (price: string | bigint, resolution: number) => { export const getPriceLevel = (price: string | bigint, resolution: number) => {
@ -26,25 +22,6 @@ export const getPriceLevel = (price: string | bigint, resolution: number) => {
return priceLevel.toString(); return priceLevel.toString();
}; };
const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({
cumulativeVol: Math.max(
orderbookData[0]?.cumulativeVol.value,
orderbookData[orderbookData.length - 1]?.cumulativeVol.value
),
});
// round instead of ceil so we will not show 0 if value if different than 0
const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100);
const updateRelativeData = (data: OrderbookRowData[]) => {
const { cumulativeVol } = getMaxVolumes(data);
data.forEach((data, i) => {
data.cumulativeVol.relativeValue = toPercentValue(
data.cumulativeVol.value / cumulativeVol
);
});
};
const updateCumulativeVolumeByType = ( const updateCumulativeVolumeByType = (
data: OrderbookRowData[], data: OrderbookRowData[],
dataType: VolumeType dataType: VolumeType
@ -53,14 +30,13 @@ const updateCumulativeVolumeByType = (
const maxIndex = data.length - 1; const maxIndex = data.length - 1;
if (dataType === VolumeType.bid) { if (dataType === VolumeType.bid) {
for (let i = 0; i <= maxIndex; i++) { for (let i = 0; i <= maxIndex; i++) {
data[i].cumulativeVol.value = data[i].cumulativeVol =
data[i].value + (i !== 0 ? data[i - 1].cumulativeVol.value : 0); data[i].volume + (i !== 0 ? data[i - 1].cumulativeVol : 0);
} }
} else { } else {
for (let i = maxIndex; i >= 0; i--) { for (let i = maxIndex; i >= 0; i--) {
data[i].cumulativeVol.value = data[i].cumulativeVol =
data[i].value + data[i].volume + (i !== maxIndex ? data[i + 1].cumulativeVol : 0);
(i !== maxIndex ? data[i + 1].cumulativeVol.value : 0);
} }
} }
} }
@ -75,6 +51,7 @@ export const compactRows = (
getPriceLevel(row.price, resolution) getPriceLevel(row.price, resolution)
); );
const orderbookData: OrderbookRowData[] = []; const orderbookData: OrderbookRowData[] = [];
Object.keys(groupedByLevel).forEach((price) => { Object.keys(groupedByLevel).forEach((price) => {
const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment; const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment;
let value = Number(volume); let value = Number(volume);
@ -83,7 +60,11 @@ export const compactRows = (
value += Number(subRow.volume); value += Number(subRow.volume);
subRow = groupedByLevel[price].pop(); subRow = groupedByLevel[price].pop();
} }
orderbookData.push({ price, value, cumulativeVol: { value: 0 } }); orderbookData.push({
price,
volume: value,
cumulativeVol: 0,
});
}); });
orderbookData.sort((a, b) => { orderbookData.sort((a, b) => {
@ -95,8 +76,9 @@ export const compactRows = (
} }
return 1; return 1;
}); });
updateCumulativeVolumeByType(orderbookData, dataType); updateCumulativeVolumeByType(orderbookData, dataType);
updateRelativeData(orderbookData);
return orderbookData; return orderbookData;
}; };
@ -140,7 +122,7 @@ export interface MockDataGeneratorParams {
numberOfSellRows: number; numberOfSellRows: number;
numberOfBuyRows: number; numberOfBuyRows: number;
overlap: number; overlap: number;
midPrice?: string; lastTradedPrice: string;
bestStaticBidPrice: number; bestStaticBidPrice: number;
bestStaticOfferPrice: number; bestStaticOfferPrice: number;
} }
@ -148,14 +130,14 @@ export interface MockDataGeneratorParams {
export const generateMockData = ({ export const generateMockData = ({
numberOfSellRows, numberOfSellRows,
numberOfBuyRows, numberOfBuyRows,
midPrice, lastTradedPrice,
overlap, overlap,
bestStaticBidPrice, bestStaticBidPrice,
bestStaticOfferPrice, bestStaticOfferPrice,
}: MockDataGeneratorParams) => { }: MockDataGeneratorParams) => {
let matrix = new Array(numberOfSellRows).fill(undefined); let matrix = new Array(numberOfSellRows).fill(undefined);
let price = let price =
Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1); Number(lastTradedPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({ const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({
price: (price -= 1).toString(), price: (price -= 1).toString(),
volume: (numberOfSellRows - i + 1).toString(), volume: (numberOfSellRows - i + 1).toString(),
@ -171,7 +153,7 @@ export const generateMockData = ({
return { return {
asks: sell, asks: sell,
bids: buy, bids: buy,
midPrice, lastTradedPrice,
bestStaticBidPrice: bestStaticBidPrice.toString(), bestStaticBidPrice: bestStaticBidPrice.toString(),
bestStaticOfferPrice: bestStaticOfferPrice.toString(), bestStaticOfferPrice: bestStaticOfferPrice.toString(),
}; };

View File

@ -17,7 +17,7 @@ export type OrderbookData = {
interface OrderbookManagerProps { interface OrderbookManagerProps {
marketId: string; marketId: string;
onClick?: (args: { price?: string; size?: string }) => void; onClick: (args: { price?: string; size?: string }) => void;
} }
export const OrderbookManager = ({ export const OrderbookManager = ({
@ -61,15 +61,17 @@ export const OrderbookManager = ({
data={data} data={data}
reload={reload} reload={reload}
> >
<Orderbook {market && marketData && (
bids={data?.depth.buy ?? []} <Orderbook
asks={data?.depth.sell ?? []} bids={data?.depth.buy ?? []}
decimalPlaces={market?.decimalPlaces ?? 0} asks={data?.depth.sell ?? []}
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0} decimalPlaces={market.decimalPlaces}
assetSymbol={market?.tradableInstrument.instrument.product.quoteName} positionDecimalPlaces={market.positionDecimalPlaces}
onClick={onClick} assetSymbol={market.tradableInstrument.instrument.product.quoteName}
midPrice={marketData?.midPrice} onClick={onClick}
/> lastTradedPrice={marketData.lastTradedPrice}
/>
)}
</AsyncRenderer> </AsyncRenderer>
); );
}; };

View File

@ -1,158 +1,149 @@
import React, { memo } from 'react'; import type { ReactNode } from 'react';
import { memo } from 'react';
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils'; import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { NumericCell, PriceCell } from '@vegaprotocol/datagrid'; import { NumericCell } from '@vegaprotocol/datagrid';
import { VolumeType } from './orderbook-data'; import { VolumeType } from './orderbook-data';
import classNames from 'classnames'; import classNames from 'classnames';
const HIDE_VOL_WIDTH = 190;
const HIDE_CUMULATIVE_VOL_WIDTH = 260;
interface OrderbookRowProps { interface OrderbookRowProps {
value: number; volume: number;
cumulativeValue?: number; cumulativeVolume: number;
cumulativeRelativeValue?: number;
decimalPlaces: number; decimalPlaces: number;
positionDecimalPlaces: number; positionDecimalPlaces: number;
price: string; price: string;
onClick?: (args: { price?: string; size?: string }) => void; onClick: (args: { price?: string; size?: string }) => void;
type: VolumeType; type: VolumeType;
width: number; width: number;
maxVol: number;
} }
const HIDE_VOL_WIDTH = 150; export const OrderbookRow = memo(
const HIDE_CUMULATIVE_VOL_WIDTH = 220;
const CumulationBar = ({
cumulativeValue = 0,
type,
}: {
cumulativeValue?: number;
type: VolumeType;
}) => {
return (
<div
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
className={classNames(
'absolute top-0 left-0 h-full',
type === VolumeType.bid
? 'bg-market-green-300 dark:bg-market-green/50'
: 'bg-market-red-300 dark:bg-market-red/30'
)}
style={{
width: `${cumulativeValue}%`,
}}
/>
);
};
const CumulativeVol = memo(
({ ({
testId, volume,
positionDecimalPlaces, cumulativeVolume,
cumulativeValue,
onClick,
}: {
ask?: number;
bid?: number;
cumulativeValue?: number;
testId?: string;
className?: string;
positionDecimalPlaces: number;
onClick?: (size?: string | number) => void;
}) => {
const volume = cumulativeValue ? (
<NumericCell
testId={testId}
value={cumulativeValue}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeValue,
positionDecimalPlaces ?? 0
)}
/>
) : null;
return onClick && volume ? (
<button
onClick={() => onClick(cumulativeValue)}
className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right pr-1"
>
{volume}
</button>
) : (
<div className="pr-1" data-testid={testId}>
{volume}
</div>
);
}
);
CumulativeVol.displayName = 'OrderBookCumulativeVol';
export const OrderbookRow = React.memo(
({
value,
cumulativeValue,
cumulativeRelativeValue,
decimalPlaces, decimalPlaces,
positionDecimalPlaces, positionDecimalPlaces,
price, price,
onClick, onClick,
type, type,
width, width,
maxVol,
}: OrderbookRowProps) => { }: OrderbookRowProps) => {
const txtId = type === VolumeType.bid ? 'bid' : 'ask'; const txtId = type === VolumeType.bid ? 'bid' : 'ask';
const cols = const cols =
width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1; width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1;
return ( return (
<div className="relative pr-1"> <div className="relative px-1">
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} /> <CumulationBar
cumulativeVolume={cumulativeVolume}
type={type}
maxVol={maxVol}
/>
<div <div
data-testid={`${txtId}-rows-container`} data-testid={`${txtId}-rows-container`}
className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)} className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)}
> >
<PriceCell <OrderBookRowCell
testId={`price-${price}`} onClick={() => onClick({ price: addDecimal(price, decimalPlaces) })}
value={BigInt(price)} >
onClick={() => <NumericCell
onClick && onClick({ price: addDecimal(price, decimalPlaces) }) testId={`price-${price}`}
} value={BigInt(price)}
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
className={
type === VolumeType.ask
? 'text-market-red dark:text-market-red'
: 'text-market-green-600 dark:text-market-green'
}
/>
{width >= HIDE_VOL_WIDTH && (
<PriceCell
testId={`${txtId}-vol-${price}`}
onClick={(value) =>
onClick &&
value &&
onClick({
size: addDecimal(value, positionDecimalPlaces),
})
}
value={value}
valueFormatted={addDecimalsFixedFormatNumber( valueFormatted={addDecimalsFixedFormatNumber(
value, price,
positionDecimalPlaces decimalPlaces
)} )}
className={classNames({
'text-market-red dark:text-market-red': type === VolumeType.ask,
'text-market-green-600 dark:text-market-green':
type === VolumeType.bid,
})}
/> />
</OrderBookRowCell>
{width >= HIDE_VOL_WIDTH && (
<OrderBookRowCell
onClick={() =>
onClick({ size: addDecimal(volume, positionDecimalPlaces) })
}
>
<NumericCell
testId={`${txtId}-vol-${price}`}
value={volume}
valueFormatted={addDecimalsFixedFormatNumber(
volume,
positionDecimalPlaces ?? 0
)}
/>
</OrderBookRowCell>
)} )}
{width >= HIDE_CUMULATIVE_VOL_WIDTH && ( {width >= HIDE_CUMULATIVE_VOL_WIDTH && (
<CumulativeVol <OrderBookRowCell
testId={`cumulative-vol-${price}`}
onClick={() => onClick={() =>
onClick &&
cumulativeValue &&
onClick({ onClick({
size: addDecimal(cumulativeValue, positionDecimalPlaces), size: addDecimal(cumulativeVolume, positionDecimalPlaces),
}) })
} }
positionDecimalPlaces={positionDecimalPlaces} >
cumulativeValue={cumulativeValue} <NumericCell
/> testId={`cumulative-vol-${price}`}
value={cumulativeVolume}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeVolume,
positionDecimalPlaces
)}
/>
</OrderBookRowCell>
)} )}
</div> </div>
</div> </div>
); );
} }
); );
OrderbookRow.displayName = 'OrderbookRow'; OrderbookRow.displayName = 'OrderbookRow';
const OrderBookRowCell = ({
children,
onClick,
}: {
children: ReactNode;
onClick: () => void;
}) => {
return (
<button
className="overflow-hidden text-right text-ellipsis whitespace-nowrap hover:dark:bg-neutral-800 hover:bg-neutral-200"
onClick={onClick}
>
{children}
</button>
);
};
const CumulationBar = ({
cumulativeVolume = 0,
type,
maxVol,
}: {
cumulativeVolume: number;
type: VolumeType;
maxVol: number;
}) => {
const width = (cumulativeVolume / maxVol) * 100;
return (
<div
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
className={classNames(
'absolute top-0 left-0 h-full',
type === VolumeType.bid
? 'bg-market-green/10 dark:bg-market-green/10'
: 'bg-market-red/10 dark:bg-market-red/10'
)}
style={{
width: `${width}%`,
}}
/>
);
};

View File

@ -1,7 +1,7 @@
import { render, waitFor, screen } from '@testing-library/react'; import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { generateMockData, VolumeType } from './orderbook-data'; import { generateMockData, VolumeType } from './orderbook-data';
import { Orderbook } from './orderbook'; import { Orderbook, OrderbookMid } from './orderbook';
import * as orderbookData from './orderbook-data'; import * as orderbookData from './orderbook-data';
function mockOffsetSize(width: number, height: number) { function mockOffsetSize(width: number, height: number) {
@ -24,7 +24,7 @@ describe('Orderbook', () => {
numberOfSellRows: 100, numberOfSellRows: 100,
numberOfBuyRows: 100, numberOfBuyRows: 100,
step: 1, step: 1,
midPrice: '122900', lastTradedPrice: '122900',
bestStaticBidPrice: 122905, bestStaticBidPrice: 122905,
bestStaticOfferPrice: 122895, bestStaticOfferPrice: 122895,
decimalPlaces: 3, decimalPlaces: 3,
@ -44,13 +44,14 @@ describe('Orderbook', () => {
positionDecimalPlaces={0} positionDecimalPlaces={0}
{...generateMockData(params)} {...generateMockData(params)}
assetSymbol="USD" assetSymbol="USD"
onClick={jest.fn()}
/> />
); );
await waitFor(() => await waitFor(() =>
screen.getByTestId(`middle-mark-price-${params.midPrice}`) screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
); );
expect( expect(
screen.getByTestId(`middle-mark-price-${params.midPrice}`) screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
).toHaveTextContent('122.90'); ).toHaveTextContent('122.90');
}); });
@ -68,10 +69,10 @@ describe('Orderbook', () => {
/> />
); );
expect( expect(
await screen.findByTestId(`middle-mark-price-${params.midPrice}`) await screen.findByTestId(`last-traded-${params.lastTradedPrice}`)
).toBeInTheDocument(); ).toBeInTheDocument();
// Before resolution change the price is 122.934 // Before resolution change the price is 122.934
await userEvent.click(await screen.getByTestId('price-122901')); await userEvent.click(screen.getByTestId('price-122901'));
expect(onClickSpy).toBeCalledWith({ price: '122.901' }); expect(onClickSpy).toBeCalledWith({ price: '122.901' });
await userEvent.click(screen.getByTestId('resolution')); await userEvent.click(screen.getByTestId('resolution'));
@ -92,7 +93,7 @@ describe('Orderbook', () => {
VolumeType.ask, VolumeType.ask,
10 10
); );
await userEvent.click(await screen.getByTestId('price-12294')); await userEvent.click(screen.getByTestId('price-12294'));
expect(onClickSpy).toBeCalledWith({ price: '122.94' }); expect(onClickSpy).toBeCalledWith({ price: '122.94' });
}); });
@ -177,3 +178,48 @@ describe('Orderbook', () => {
}); });
}); });
}); });
describe('OrderbookMid', () => {
const props = {
lastTradedPrice: '100',
decimalPlaces: 0,
assetSymbol: 'BTC',
bestAskPrice: '101',
bestBidPrice: '99',
};
it('renders no change until lastTradedPrice changes', () => {
const { rerender } = render(<OrderbookMid {...props} />);
expect(screen.getByTestId(/last-traded/)).toHaveTextContent(
props.lastTradedPrice
);
expect(screen.getByText(props.assetSymbol)).toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(2)');
// rerender with no change should not show the icon
rerender(<OrderbookMid {...props} />);
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
rerender(
<OrderbookMid {...props} lastTradedPrice="101" bestAskPrice="102" />
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(3)');
// rerender again with the same price, should still be set to 'up'
rerender(
<OrderbookMid
{...props}
lastTradedPrice="101"
bestAskPrice="102"
bestBidPrice="98"
/>
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(4)');
rerender(<OrderbookMid {...props} lastTradedPrice="100" />);
expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument();
});
});

View File

@ -9,9 +9,9 @@ type Props = Omit<MockDataGeneratorParams, 'resolution'> & {
const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => { const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
return ( return (
<div className="absolute inset-0 dark:bg-black dark:text-neutral-200 bg-white text-neutral-800"> <div className="absolute inset-0 bg-white dark:bg-black dark:text-neutral-200 text-neutral-800">
<div <div
className="absolute left-0 top-0 bottom-0" className="absolute top-0 bottom-0 left-0"
style={{ width: '400px' }} style={{ width: '400px' }}
> >
<Orderbook <Orderbook
@ -19,6 +19,7 @@ const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
decimalPlaces={decimalPlaces} decimalPlaces={decimalPlaces}
{...generateMockData({ ...props })} {...generateMockData({ ...props })}
assetSymbol="USD" assetSymbol="USD"
onClick={() => undefined}
/> />
</div> </div>
</div> </div>

View File

@ -1,25 +1,15 @@
import { useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
import { import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
addDecimalsFormatNumber,
formatNumberFixed,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { usePrevious } from '@vegaprotocol/react-helpers'; import { usePrevious } from '@vegaprotocol/react-helpers';
import { OrderbookRow } from './orderbook-row'; import { OrderbookRow } from './orderbook-row';
import type { OrderbookRowData } from './orderbook-data'; import type { OrderbookRowData } from './orderbook-data';
import { compactRows, VolumeType } from './orderbook-data'; import { compactRows, VolumeType } from './orderbook-data';
import { import { Splash, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames'; import classNames from 'classnames';
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
import { OrderbookControls } from './orderbook-controls';
// Sets row height, will be used to calculate number of rows that can be // Sets row height, will be used to calculate number of rows that can be
// displayed each side of the book without overflow // displayed each side of the book without overflow
@ -27,20 +17,7 @@ export const rowHeight = 17;
const rowGap = 1; const rowGap = 1;
const midHeight = 30; const midHeight = 30;
type PriceChange = 'up' | 'down' | 'none'; const OrderbookSide = ({
const PRICE_CHANGE_ICON_MAP: Readonly<Record<PriceChange, VegaIconNames>> = {
up: VegaIconNames.ARROW_UP,
down: VegaIconNames.ARROW_DOWN,
none: VegaIconNames.BULLET,
};
const PRICE_CHANGE_CLASS_MAP: Readonly<Record<PriceChange, string>> = {
up: 'text-market-green-600 dark:text-market-green',
down: 'text-market-red dark:text-market-red',
none: 'text-vega-blue-500',
};
const OrderbookTable = ({
rows, rows,
resolution, resolution,
type, type,
@ -48,14 +25,16 @@ const OrderbookTable = ({
positionDecimalPlaces, positionDecimalPlaces,
onClick, onClick,
width, width,
maxVol,
}: { }: {
rows: OrderbookRowData[]; rows: OrderbookRowData[];
resolution: number; resolution: number;
decimalPlaces: number; decimalPlaces: number;
positionDecimalPlaces: number; positionDecimalPlaces: number;
type: VolumeType; type: VolumeType;
onClick?: (args: { price?: string; size?: string }) => void; onClick: (args: { price?: string; size?: string }) => void;
width: number; width: number;
maxVol: number;
}) => { }) => {
return ( return (
<div <div
@ -78,11 +57,11 @@ const OrderbookTable = ({
onClick={onClick} onClick={onClick}
decimalPlaces={decimalPlaces - Math.log10(resolution)} decimalPlaces={decimalPlaces - Math.log10(resolution)}
positionDecimalPlaces={positionDecimalPlaces} positionDecimalPlaces={positionDecimalPlaces}
value={data.value} volume={data.volume}
cumulativeValue={data.cumulativeVol.value} cumulativeVolume={data.cumulativeVol}
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
type={type} type={type}
width={width} width={width}
maxVol={maxVol}
/> />
))} ))}
</div> </div>
@ -90,31 +69,88 @@ const OrderbookTable = ({
); );
}; };
export const OrderbookMid = ({
lastTradedPrice,
decimalPlaces,
assetSymbol,
bestAskPrice,
bestBidPrice,
}: {
lastTradedPrice: string;
decimalPlaces: number;
assetSymbol: string;
bestAskPrice: string;
bestBidPrice: string;
}) => {
const previousLastTradedPrice = usePrevious(lastTradedPrice);
const priceChangeRef = useRef<'up' | 'down' | 'none'>('none');
const spread = (BigInt(bestAskPrice) - BigInt(bestBidPrice)).toString();
if (previousLastTradedPrice !== lastTradedPrice) {
priceChangeRef.current =
Number(previousLastTradedPrice) > Number(lastTradedPrice) ? 'down' : 'up';
}
return (
<div className="flex items-center justify-center text-base gap-2">
{priceChangeRef.current !== 'none' && (
<span
className={classNames('flex flex-col justify-center', {
'text-market-green-600 dark:text-market-green':
priceChangeRef.current === 'up',
'text-market-red dark:text-market-red':
priceChangeRef.current === 'down',
})}
>
<VegaIcon
name={
priceChangeRef.current === 'up'
? VegaIconNames.ARROW_UP
: VegaIconNames.ARROW_DOWN
}
/>
</span>
)}
<span
// monospace sizing doesn't quite align with alpha
className="font-mono text-[15px]"
data-testid={`last-traded-${lastTradedPrice}`}
title={t('Last traded price')}
>
{addDecimalsFormatNumber(lastTradedPrice, decimalPlaces)}
</span>
<span>{assetSymbol}</span>
<span
title={t('Spread')}
className="font-mono text-xs text-muted"
data-testid="spread"
>
({addDecimalsFormatNumber(spread, decimalPlaces)})
</span>
</div>
);
};
interface OrderbookProps { interface OrderbookProps {
decimalPlaces: number; decimalPlaces: number;
positionDecimalPlaces: number; positionDecimalPlaces: number;
onClick?: (args: { price?: string; size?: string }) => void; onClick: (args: { price?: string; size?: string }) => void;
midPrice?: string; lastTradedPrice: string;
bids: PriceLevelFieldsFragment[]; bids: PriceLevelFieldsFragment[];
asks: PriceLevelFieldsFragment[]; asks: PriceLevelFieldsFragment[];
assetSymbol: string | undefined; assetSymbol: string;
} }
export const Orderbook = ({ export const Orderbook = ({
decimalPlaces, decimalPlaces,
positionDecimalPlaces, positionDecimalPlaces,
onClick, onClick,
midPrice, lastTradedPrice,
asks, asks,
bids, bids,
assetSymbol, assetSymbol,
}: OrderbookProps) => { }: OrderbookProps) => {
const [resolution, setResolution] = useState(1); const [resolution, setResolution] = useState(1);
const resolutions = new Array(
Math.max(midPrice?.toString().length ?? 0, decimalPlaces + 1)
)
.fill(null)
.map((v, i) => Math.pow(10, i));
const groupedAsks = useMemo(() => { const groupedAsks = useMemo(() => {
return compactRows(asks, VolumeType.ask, resolution); return compactRows(asks, VolumeType.ask, resolution);
@ -123,44 +159,11 @@ export const Orderbook = ({
const groupedBids = useMemo(() => { const groupedBids = useMemo(() => {
return compactRows(bids, VolumeType.bid, resolution); return compactRows(bids, VolumeType.bid, resolution);
}, [bids, resolution]); }, [bids, resolution]);
const [isOpen, setOpen] = useState(false);
const previousMidPrice = usePrevious(midPrice);
const priceChangeRef = useRef<'up' | 'down' | 'none'>('none');
if (midPrice && previousMidPrice !== midPrice) {
priceChangeRef.current =
(previousMidPrice || '') > midPrice ? 'down' : 'up';
}
const priceChangeIcon = ( // get the best bid/ask, note that we are using the pre aggregated
<span // values so we can render the most accurate spread in the mid section
className={classNames(PRICE_CHANGE_CLASS_MAP[priceChangeRef.current])} const bestAskPrice = asks[0] ? asks[0].price : '0';
> const bestBidPrice = bids[0] ? bids[0].price : '0';
<VegaIcon name={PRICE_CHANGE_ICON_MAP[priceChangeRef.current]} />
</span>
);
const formatResolution = (r: number) => {
return formatNumberFixed(
Math.log10(r) - decimalPlaces > 0
? Math.pow(10, Math.log10(r) - decimalPlaces)
: 0,
decimalPlaces - Math.log10(r)
);
};
const increaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index < resolutions.length - 1) {
setResolution(resolutions[index + 1]);
}
};
const decreaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index > 0) {
setResolution(resolutions[index - 1]);
}
};
return ( return (
<div className="h-full text-xs grid grid-rows-[1fr_min-content]"> <div className="h-full text-xs grid grid-rows-[1fr_min-content]">
@ -171,21 +174,30 @@ export const Orderbook = ({
1, 1,
Math.floor((height - midHeight) / 2 / (rowHeight + rowGap)) Math.floor((height - midHeight) / 2 / (rowHeight + rowGap))
); );
const askRows = groupedAsks?.slice(limit * -1) ?? []; const askRows = groupedAsks.slice(limit * -1);
const bidRows = groupedBids?.slice(0, limit) ?? []; const bidRows = groupedBids.slice(0, limit);
// this is used for providing a scale to render the volume
// bars based on the visible book
const deepestVisibleAsk = askRows[0];
const deepestVisibleBid = bidRows[bidRows.length - 1];
const maxVol = Math.max(
deepestVisibleAsk?.cumulativeVol || 0,
deepestVisibleBid?.cumulativeVol || 0
);
return ( return (
<div <div
className="overflow-hidden grid" className="overflow-hidden grid"
data-testid="orderbook-grid-element" data-testid="orderbook-grid-element"
style={{ style={{
width: width + 'px', width,
height: height + 'px', height,
gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation
}} }}
> >
{askRows.length || bidRows.length ? ( {askRows.length || bidRows.length ? (
<> <>
<OrderbookTable <OrderbookSide
rows={askRows} rows={askRows}
type={VolumeType.ask} type={VolumeType.ask}
resolution={resolution} resolution={resolution}
@ -193,22 +205,16 @@ export const Orderbook = ({
positionDecimalPlaces={positionDecimalPlaces} positionDecimalPlaces={positionDecimalPlaces}
onClick={onClick} onClick={onClick}
width={width} width={width}
maxVol={maxVol}
/> />
<div className="flex items-center justify-center gap-2"> <OrderbookMid
{midPrice && ( lastTradedPrice={lastTradedPrice}
<> decimalPlaces={decimalPlaces}
<span assetSymbol={assetSymbol}
className="font-mono text-lg" bestAskPrice={bestAskPrice}
data-testid={`middle-mark-price-${midPrice}`} bestBidPrice={bestBidPrice}
> />
{addDecimalsFormatNumber(midPrice, decimalPlaces)} <OrderbookSide
</span>
<span className="text-base">{assetSymbol}</span>
{priceChangeIcon}
</>
)}
</div>
<OrderbookTable
rows={bidRows} rows={bidRows}
type={VolumeType.bid} type={VolumeType.bid}
resolution={resolution} resolution={resolution}
@ -216,10 +222,11 @@ export const Orderbook = ({
positionDecimalPlaces={positionDecimalPlaces} positionDecimalPlaces={positionDecimalPlaces}
onClick={onClick} onClick={onClick}
width={width} width={width}
maxVol={maxVol}
/> />
</> </>
) : ( ) : (
<div className="inset-0 absolute"> <div className="absolute inset-0">
<Splash>{t('No data')}</Splash> <Splash>{t('No data')}</Splash>
</div> </div>
)} )}
@ -228,59 +235,13 @@ export const Orderbook = ({
}} }}
</ReactVirtualizedAutoSizer> </ReactVirtualizedAutoSizer>
</div> </div>
<div className="border-t border-default flex"> <div className="border-t border-default">
<button <OrderbookControls
onClick={increaseResolution} lastTradedPrice={lastTradedPrice}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1} resolution={resolution}
className="flex items-center border-r border-default px-2 cursor-pointer" decimalPlaces={decimalPlaces}
data-testid="plus-button" setResolution={setResolution}
> />
<VegaIcon size={12} name={VegaIconNames.PLUS} />
</button>
<DropdownMenu
open={isOpen}
onOpenChange={(open) => setOpen(open)}
trigger={
<DropdownMenuTrigger
data-testid="resolution"
className="flex justify-between px-1 items-center"
style={{
width: `${
Math.max.apply(
null,
resolutions.map((item) => formatResolution(item).length)
) + 3
}ch`,
}}
>
<VegaIcon
size={12}
name={
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
<div className="text-xs text-left">
{formatResolution(resolution)}
</div>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align="start">
{resolutions.map((r) => (
<DropdownMenuItem key={r} onClick={() => setResolution(r)}>
{formatResolution(r)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center border-x border-default px-2 cursor-pointer"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
</button>
</div> </div>
</div> </div>
); );

View File

@ -3,23 +3,23 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataUpdateSubscriptionVariables = Types.Exact<{ export type MarketDataUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketId: Types.Scalars['ID'];
}>; }>;
export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> }; export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> };
export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataQueryVariables = Types.Exact<{ export type MarketDataQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketId: Types.Scalars['ID'];
}>; }>;
export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null }; export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null };
export const MarketDataUpdateFieldsFragmentDoc = gql` export const MarketDataUpdateFieldsFragmentDoc = gql`
fragment MarketDataUpdateFields on ObservableMarketData { fragment MarketDataUpdateFields on ObservableMarketData {
@ -56,6 +56,7 @@ export const MarketDataUpdateFieldsFragmentDoc = gql`
suppliedStake suppliedStake
targetStake targetStake
trigger trigger
lastTradedPrice
} }
`; `;
export const MarketDataFieldsFragmentDoc = gql` export const MarketDataFieldsFragmentDoc = gql`
@ -95,6 +96,7 @@ export const MarketDataFieldsFragmentDoc = gql`
suppliedStake suppliedStake
targetStake targetStake
trigger trigger
lastTradedPrice
} }
`; `;
export const MarketDataUpdateDocument = gql` export const MarketDataUpdateDocument = gql`

View File

@ -32,6 +32,7 @@ fragment MarketDataUpdateFields on ObservableMarketData {
suppliedStake suppliedStake
targetStake targetStake
trigger trigger
lastTradedPrice
} }
subscription MarketDataUpdate($marketId: ID!) { subscription MarketDataUpdate($marketId: ID!) {
@ -76,6 +77,7 @@ fragment MarketDataFields on MarketData {
suppliedStake suppliedStake
targetStake targetStake
trigger trigger
lastTradedPrice
} }
query MarketData($marketId: ID!) { query MarketData($marketId: ID!) {

View File

@ -62,6 +62,7 @@ const marketDataFields: MarketDataFieldsFragment = {
markPrice: '4612690058', markPrice: '4612690058',
midPrice: '4612690000', midPrice: '4612690000',
openInterest: '0', openInterest: '0',
lastTradedPrice: '4612690000',
priceMonitoringBounds: [ priceMonitoringBounds: [
{ {
minValidPrice: '654701', minValidPrice: '654701',
@ -99,6 +100,7 @@ const marketDataUpdateFields: MarketDataUpdateFieldsFragment = {
marketValueProxy: '', marketValueProxy: '',
markPrice: '4612690058', markPrice: '4612690058',
midPrice: '0', midPrice: '0',
lastTradedPrice: '0',
openInterest: '0', openInterest: '0',
staticMidPrice: '0', staticMidPrice: '0',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED, trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,

View File

@ -11,4 +11,7 @@ As a market user I want to see information about orders existing in the market.
- I **Must** see prices sorted descending (<a name="6003-ORDB-007" href="#6003-ORDB-007">6003-ORDB-007</a>) - I **Must** see prices sorted descending (<a name="6003-ORDB-007" href="#6003-ORDB-007">6003-ORDB-007</a>)
- I **Must** be able to set a resolution of data (<a name="6003-ORDB-008" href="#6003-ORDB-008">6003-ORDB-008</a>) - I **Must** be able to set a resolution of data (<a name="6003-ORDB-008" href="#6003-ORDB-008">6003-ORDB-008</a>)
- When I click specific price, it **Must** be copied to deal ticket form (<a name="6003-ORDB-009" href="#6003-ORDB-009">6003-ORDB-009</a>) - When I click specific price, it **Must** be copied to deal ticket form (<a name="6003-ORDB-009" href="#6003-ORDB-009">6003-ORDB-009</a>)
- Order is removed from orderbook if traded away(<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>) - Order is removed from orderbook if traded away (<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>)
- Spread (bestAsk - bestOffer) is show in the mid secion (<a name="6003-ORDB-011" href="#6003-ORDB-011">6003-ORDB-011</a>)
- Cumulative volume is displayed visually (volume bars) as a proportion of the entire book volume (<a name="6003-ORDB-012" href="#6003-ORDB-012">6003-ORDB-012</a>)
- Mid section shows the last traded price movement using an arrow (<a name="6003-ORDB-013" href="#6003-ORDB-013">6003-ORDB-013</a>)