fix(trading): add missing funding panel, fix 24 hr price/volume change (#5788)

Co-authored-by: Ben <ben@vega.xyz>
This commit is contained in:
Matthew Russell 2024-02-15 10:41:04 -05:00 committed by GitHub
parent 4ad1a47ded
commit 8268acac6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 181 additions and 79 deletions

View File

@ -56,7 +56,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<Last24hPriceChange <Last24hPriceChange
marketId={market.id} marketId={market.id}
decimalPlaces={market.decimalPlaces} decimalPlaces={market.decimalPlaces}
fallback={<span>-</span>}
/> />
</HeaderStat> </HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume"> <HeaderStat heading={t('Volume (24h)')} testId="market-volume">

View File

@ -72,7 +72,14 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
<div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]"> <div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
<div className="flex flex-col w-full overflow-hidden"> <div className="flex flex-col w-full overflow-hidden">
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default"> <div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{['chart', 'orderbook', 'trades', 'liquidity', 'fundingPayments'] {[
'chart',
'orderbook',
'trades',
'liquidity',
'fundingPayments',
'funding',
]
// filter to control available views for the current market // filter to control available views for the current market
// e.g. only perpetuals should get the funding views // e.g. only perpetuals should get the funding views
.filter((_key) => { .filter((_key) => {
@ -163,9 +170,12 @@ const ViewButton = ({
onClick: () => void; onClick: () => void;
}) => { }) => {
const label = useViewLabel(view); const label = useViewLabel(view);
const className = classNames('py-2 px-4 min-w-[100px] capitalize text-sm', { const className = classNames(
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive, 'py-2 px-4 capitalize text-sm whitespace-nowrap',
}); {
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
}
);
return ( return (
<button data-testid={view} onClick={onClick} className={className}> <button data-testid={view} onClick={onClick} className={className}>
@ -181,8 +191,8 @@ const useViewLabel = (view: TradingView) => {
chart: t('Chart'), chart: t('Chart'),
depth: t('Depth'), depth: t('Depth'),
liquidity: t('Liquidity'), liquidity: t('Liquidity'),
funding: t('Funding'), funding: t('Funding history'),
fundingPayments: t('Funding'), fundingPayments: t('Funding payments'),
orderbook: t('Orderbook'), orderbook: t('Orderbook'),
trades: t('Trades'), trades: t('Trades'),
positions: t('Positions'), positions: t('Positions'),

View File

@ -29,28 +29,35 @@ export const MobileMarketHeader = () => {
if (!marketId) return null; if (!marketId) return null;
return ( return (
<div className="pl-3 pr-2 flex justify-between gap-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700"> <div className="pl-3 pr-2 grid grid-cols-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
<FullScreenPopover <FullScreenPopover
open={openMarket} open={openMarket}
onOpenChange={(x) => { onOpenChange={(x) => {
setOpenMarket(x); setOpenMarket(x);
}} }}
trigger={ trigger={
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-base leading-3 md:text-lg whitespace-nowrap"> <button
{data data-testid="popover-trigger"
? data.tradableInstrument.instrument.code className="min-w-0 flex gap-1 items-center"
: t('Select market')} >
<span <h1 className="whitespace-nowrap overflow-hidden text-ellipsis items-center">
<span className="">
{data
? data.tradableInstrument.instrument.code
: t('Select market')}
</span>
</h1>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames( className={classNames(
'transition-transform ease-in-out duration-300 flex', 'origin-center transition-transform ease-in-out duration-300 flex',
{ {
'rotate-180': openMarket, 'rotate-180': openMarket,
} }
)} )}
> />
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={16} /> </button>
</span>
</h1>
} }
> >
<MarketSelector <MarketSelector
@ -64,34 +71,40 @@ export const MobileMarketHeader = () => {
setOpenPrice(x); setOpenPrice(x);
}} }}
trigger={ trigger={
<span className="flex gap-2 items-end md:text-md whitespace-nowrap leading-3"> <button
data-testid="popover-trigger"
className="min-w-0 flex gap-2 items-center justify-end"
>
{data && ( {data && (
<> <>
<span className="text-xs"> <span className="min-w-0 flex flex-col items-end gap-0">
<Last24hPriceChange <span className="text-sm">
marketId={data.id} <MarketMarkPrice
decimalPlaces={data.decimalPlaces} marketId={data.id}
/> decimalPlaces={data.decimalPlaces}
</span> />
<span className="flex items-center gap-1"> </span>
<MarketMarkPrice <span className="text-xs">
marketId={data.id} <Last24hPriceChange
decimalPlaces={data.decimalPlaces} marketId={data.id}
/> decimalPlaces={data.decimalPlaces}
<VegaIcon fallback={<span />} // dont render anything so price is vertically centered
name={VegaIconNames.CHEVRON_DOWN} />
size={16} </span>
className={classNames(
'transition-transform ease-in-out duration-300',
{
'rotate-180': openPrice,
}
)}
/>
</span> </span>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'min-w-0 transition-transform ease-in-out duration-300',
{
'rotate-180': openPrice,
}
)}
/>
</> </>
)} )}
</span> </button>
} }
> >
{data && ( {data && (
@ -104,11 +117,11 @@ export const MobileMarketHeader = () => {
); );
}; };
export interface PopoverProps extends PopoverPrimitive.PopoverProps { interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string; trigger: React.ReactNode | string;
} }
export const FullScreenPopover = ({ const FullScreenPopover = ({
trigger, trigger,
children, children,
open, open,
@ -116,7 +129,7 @@ export const FullScreenPopover = ({
}: PopoverProps) => { }: PopoverProps) => {
return ( return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}> <PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger data-testid="popover-trigger"> <PopoverPrimitive.Trigger asChild={true}>
{trigger} {trigger}
</PopoverPrimitive.Trigger> </PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>

View File

@ -35,9 +35,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
# 6002-MDET-003 # 6002-MDET-003
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00") expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
# 6002-MDET-004 # 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00") expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)-")
# 6002-MDET-005 # 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)") expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008 # 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text( expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
"Settlement assettDAI" "Settlement assettDAI"

View File

@ -7,7 +7,7 @@ import {
type FirstDataRenderedEvent, type FirstDataRenderedEvent,
type SortChangedEvent, type SortChangedEvent,
type GridReadyEvent, type GridReadyEvent,
GridApi, type GridApi,
} from 'ag-grid-community'; } from 'ag-grid-community';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';

View File

@ -2,7 +2,7 @@
"{{liquidityPriceRange}} of mid price": "{{liquidityPriceRange}} of mid price", "{{liquidityPriceRange}} of mid price": "{{liquidityPriceRange}} of mid price",
"{{probability}} probability price bounds": "{{probability}} probability price bounds", "{{probability}} probability price bounds": "{{probability}} probability price bounds",
"24 hour change is unavailable at this time. The price change in the last 120 hours is:": "24 hour change is unavailable at this time. The price change in the last 120 hours is:", "24 hour change is unavailable at this time. The price change in the last 120 hours is:": "24 hour change is unavailable at this time. The price change in the last 120 hours is:",
"24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}": "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}", "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
"A concept derived from traditional markets. It is a calculated value for the current market price on a market.": "A concept derived from traditional markets. It is a calculated value for the current market price on a market.", "A concept derived from traditional markets. It is a calculated value for the current market price on a market.": "A concept derived from traditional markets. It is a calculated value for the current market price on a market.",
"A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.": "A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.", "A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.": "A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.",
"A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.": "A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.", "A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.": "A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.",
@ -49,6 +49,9 @@
"Market": "Market", "Market": "Market",
"Market data": "Market data", "Market data": "Market data",
"Market governance": "Market governance", "Market governance": "Market governance",
"Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:",
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:",
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
"Market ID": "Market ID", "Market ID": "Market ID",
"Market price": "Market price", "Market price": "Market price",
"Market specification": "Market specification", "Market specification": "Market specification",

View File

@ -1,7 +1,8 @@
import { type ReactNode } from 'react';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
formatNumberPercentage, formatNumberPercentage,
isNumeric, getDateTimeFormat,
priceChange, priceChange,
priceChangePercentage, priceChangePercentage,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
@ -14,29 +15,57 @@ import { useT } from '../../use-t';
interface Props { interface Props {
marketId?: string; marketId?: string;
decimalPlaces?: number; decimalPlaces: number;
initialValue?: string[]; fallback?: ReactNode;
isHeader?: boolean;
noUpdate?: boolean;
// render prop for no price change
fallback?: React.ReactNode;
} }
export const Last24hPriceChange = ({ export const Last24hPriceChange = ({
marketId, marketId,
decimalPlaces, decimalPlaces,
initialValue,
fallback, fallback,
}: Props) => { }: Props) => {
const t = useT(); const t = useT();
const { oneDayCandles, error, fiveDaysCandles } = useCandles({ const { oneDayCandles, fiveDaysCandles, error } = useCandles({
marketId, marketId,
}); });
if (
fiveDaysCandles && const nonIdeal = fallback || <span>{'-'}</span>;
fiveDaysCandles.length > 0 &&
(!oneDayCandles || oneDayCandles?.length === 0) if (error || !oneDayCandles || !fiveDaysCandles) {
) { return nonIdeal;
}
if (fiveDaysCandles.length < 24) {
return (
<Tooltip
description={
<span className="justify-start">
{t(
'Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:',
{
start: getDateTimeFormat().format(
new Date(fiveDaysCandles[0].periodStart)
),
end: getDateTimeFormat().format(
new Date(
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
)
),
}
)}
<PriceChangeCell
candles={fiveDaysCandles.map((c) => c.close) || []}
decimalPlaces={decimalPlaces}
/>
</span>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
if (oneDayCandles.length < 24) {
return ( return (
<Tooltip <Tooltip
description={ description={
@ -51,16 +80,12 @@ export const Last24hPriceChange = ({
</span> </span>
} }
> >
<span>{fallback}</span> <span>{nonIdeal}</span>
</Tooltip> </Tooltip>
); );
} }
if (error || !isNumeric(decimalPlaces)) { const candles = oneDayCandles?.map((c) => c.close) || [];
return <span>{fallback}</span>;
}
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
const change = priceChange(candles); const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles); const changePercentage = priceChangePercentage(candles);

View File

@ -2,6 +2,7 @@ import { calcCandleVolume, calcCandleVolumePrice } from '../../market-utils';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
formatNumber, formatNumber,
getDateTimeFormat,
isNumeric, isNumeric,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { Tooltip } from '@vegaprotocol/ui-toolkit'; import { Tooltip } from '@vegaprotocol/ui-toolkit';
@ -26,15 +27,17 @@ export const Last24hVolume = ({
quoteUnit, quoteUnit,
}: Props) => { }: Props) => {
const t = useT(); const t = useT();
const { oneDayCandles, fiveDaysCandles } = useCandles({ const { oneDayCandles, fiveDaysCandles, error } = useCandles({
marketId, marketId,
}); });
if ( const nonIdeal = <span>{'-'}</span>;
fiveDaysCandles &&
fiveDaysCandles.length > 0 && if (error || !oneDayCandles || !fiveDaysCandles) {
(!oneDayCandles || oneDayCandles?.length === 0) return nonIdeal;
) { }
if (fiveDaysCandles.length < 24) {
const candleVolume = calcCandleVolume(fiveDaysCandles); const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumePrice = calcCandleVolumePrice( const candleVolumePrice = calcCandleVolumePrice(
fiveDaysCandles, fiveDaysCandles,
@ -55,14 +58,59 @@ export const Last24hVolume = ({
<div> <div>
<span className="flex flex-col"> <span className="flex flex-col">
{t( {t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} ({{candleVolumePrice}} {{quoteUnit}})', 'Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}',
{
start: getDateTimeFormat().format(
new Date(fiveDaysCandles[0].periodStart)
),
end: getDateTimeFormat().format(
new Date(
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
)
),
candleVolumeValue,
candleVolumePrice,
quoteUnit,
}
)}
</span>
</div>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
if (oneDayCandles.length < 24) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumePrice = calcCandleVolumePrice(
fiveDaysCandles,
marketDecimals,
positionDecimalPlaces
);
const candleVolumeValue =
candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-';
return (
<Tooltip
description={
<div>
<span className="flex flex-col">
{t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of ({{candleVolumePrice}} {{quoteUnit}})',
{ candleVolumeValue, candleVolumePrice, quoteUnit } { candleVolumeValue, candleVolumePrice, quoteUnit }
)} )}
</span> </span>
</div> </div>
} }
> >
<span>-</span> <span>{nonIdeal}</span>
</Tooltip> </Tooltip>
); );
} }

View File

@ -15,7 +15,6 @@ const mockData = [
periodStart: today.toISOString(), periodStart: today.toISOString(),
__typename: 'Candle', __typename: 'Candle',
}, },
null,
{ {
high: '6309988', high: '6309988',
low: '6296335', low: '6296335',

View File

@ -18,7 +18,12 @@ export const useCandles = ({ marketId }: { marketId?: string }) => {
skip: !marketId, skip: !marketId,
}); });
const fiveDaysCandles = data?.filter(Boolean); const fiveDaysCandles = data?.filter((c) => {
if (c.open === '' || c.close === '' || c.high === '' || c.close === '') {
return false;
}
return true;
});
const oneDayCandles = fiveDaysCandles?.filter((candle) => const oneDayCandles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday) isCandleLessThan24hOld(candle, yesterday)

View File

@ -1,7 +1,7 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { import {
OpenVolumeData,
openVolumeDataProvider, openVolumeDataProvider,
type OpenVolumeData,
} from './positions-data-providers'; } from './positions-data-providers';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';