feat(trading): orderbook changes (#4652)
This commit is contained in:
parent
255c3752f2
commit
559ef48d6d
@ -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';
|
||||||
|
@ -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 = [
|
||||||
|
109
libs/market-depth/src/lib/orderbook-controls.tsx
Normal file
109
libs/market-depth/src/lib/orderbook-controls.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
10
libs/markets/src/lib/__generated__/market-data.ts
generated
10
libs/markets/src/lib/__generated__/market-data.ts
generated
@ -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`
|
||||||
|
@ -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!) {
|
||||||
|
@ -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,
|
||||||
|
@ -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>)
|
||||||
|
Loading…
Reference in New Issue
Block a user