feat(trading): quote unit and settlement asset relation (#2953)

This commit is contained in:
m.ray 2023-02-22 12:05:31 -05:00 committed by GitHub
parent efeccc7972
commit 3aefa10a4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 182 additions and 108 deletions

View File

@ -22,8 +22,7 @@ export const MarketDetails = ({
}: { }: {
market: MarketInfoNoCandlesQuery['market']; market: MarketInfoNoCandlesQuery['market'];
}) => { }) => {
const assetSymbol = const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
const assetId = useMemo( const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id, () => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market] [market]
@ -39,6 +38,10 @@ export const MarketDetails = ({
const assetDecimals = const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals; market.tradableInstrument.instrument.product.settlementAsset.decimals;
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const panels = [ const panels = [
{ {
title: t('Key details'), title: t('Key details'),
@ -179,13 +182,21 @@ export const MarketDetails = ({
{ {
title: t('Liquidity price range'), title: t('Liquidity price range'),
content: ( content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders count towards a commitment they have to be
within either the liquidity or price monitoring bounds (whichever is
tighter).`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
price.`}
</p>
<MarketInfoTable <MarketInfoTable
noBorder={false} noBorder={false}
data={{ data={{
liquidityPriceRange: formatNumberPercentage( liquidityPriceRange: `${liquidityPriceRange} of mid price`,
new BigNumber(market.lpPriceRange).times(100) lowestPrice:
),
LPVolumeMin:
market.data?.midPrice && market.data?.midPrice &&
`${addDecimalsFormatNumber( `${addDecimalsFormatNumber(
new BigNumber(1) new BigNumber(1)
@ -193,8 +204,8 @@ export const MarketDetails = ({
.times(market.data.midPrice) .times(market.data.midPrice)
.toString(), .toString(),
market.decimalPlaces market.decimalPlaces
)} ${assetSymbol}`, )} ${quoteUnit}`,
LPVolumeMax: highestPrice:
market.data?.midPrice && market.data?.midPrice &&
`${addDecimalsFormatNumber( `${addDecimalsFormatNumber(
new BigNumber(1) new BigNumber(1)
@ -202,9 +213,10 @@ export const MarketDetails = ({
.times(market.data.midPrice) .times(market.data.midPrice)
.toString(), .toString(),
market.decimalPlaces market.decimalPlaces
)} ${assetSymbol}`, )} ${quoteUnit}`,
}} }}
></MarketInfoTable> ></MarketInfoTable>
</>
), ),
}, },
{ {

View File

@ -29,6 +29,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(0, 'Mark Price', '0.05749'); validateMarketDataRow(0, 'Mark Price', '0.05749');
validateMarketDataRow(1, 'Best Bid Price', '6.81765 '); validateMarketDataRow(1, 'Best Bid Price', '6.81765 ');
validateMarketDataRow(2, 'Best Offer Price', '6.81769 '); validateMarketDataRow(2, 'Best Offer Price', '6.81769 ');
validateMarketDataRow(3, 'Quote Unit', 'BTC');
}); });
it('market volume displayed', () => { it('market volume displayed', () => {
@ -168,9 +169,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('liquidity price range displayed', () => { it('liquidity price range displayed', () => {
cy.getByTestId(marketTitle).contains('Liquidity price range').click(); cy.getByTestId(marketTitle).contains('Liquidity price range').click();
validateMarketDataRow(0, 'Liquidity Price Range', '2.00%'); validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
validateMarketDataRow(1, 'LP Volume Min', '0.05634 tBTC'); validateMarketDataRow(1, 'Lowest Price', '0.05634 BTC');
validateMarketDataRow(2, 'LP Volume Max', '0.05864 tBTC'); validateMarketDataRow(2, 'Highest Price', '0.05864 BTC');
}); });
it('oracle displayed', () => { it('oracle displayed', () => {

View File

@ -37,7 +37,7 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
cy.getByTestId(form).find('#wallet').click().type('invalid name'); cy.getByTestId(form).find('#wallet').click().type('invalid name');
cy.getByTestId(form).find('#passphrase').click().type('invalid password'); cy.getByTestId(form).find('#passphrase').click().type('invalid password');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click(); cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId('form-error').should('have.text', 'Invalid credentials'); cy.getByTestId('form-error').should('have.text', 'No wallet detected');
}); });
it('doesnt connect with invalid fields', () => { it('doesnt connect with invalid fields', () => {

View File

@ -123,7 +123,7 @@ export const MarketLiquiditySupplied = ({
{showMessage && ( {showMessage && (
<p className="mt-4"> <p className="mt-4">
{t( {t(
'The market is in an auction because there are no priced limit orders, which are required to deploy liquidity commitment pegged orders. This means the order book is empty on one or both sides.' 'The market has sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
)} )}
</p> </p>
)} )}

View File

@ -58,6 +58,7 @@ export const AssetDetailsDialog = ({
const { data } = useAssetsDataProvider(); const { data } = useAssetsDataProvider();
const asset = data?.find((a) => a.id === assetId); const asset = data?.find((a) => a.id === assetId);
const assetSymbol = asset?.symbol || '';
const content = asset ? ( const content = asset ? (
<div className="my-2"> <div className="my-2">
@ -97,6 +98,12 @@ export const AssetDetailsDialog = ({
}} }}
> >
{content} {content}
<p className="text-sm mb-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
)}
</p>
<div className="w-1/4"> <div className="w-1/4">
<Button <Button
data-testid="close-asset-details-dialog" data-testid="close-asset-details-dialog"

View File

@ -48,7 +48,7 @@ export const DealTicketEstimates = ({
<DataTitle quoteName={quoteName}>{t('Est. Position Size')}</DataTitle> <DataTitle quoteName={quoteName}>{t('Est. Position Size')}</DataTitle>
<ValueTooltipRow <ValueTooltipRow
value={notionalSize} value={notionalSize}
description={constants.NOTIONAL_SIZE_TOOLTIP_TEXT} description={constants.NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName || '')}
/> />
</div> </div>
)} )}
@ -66,7 +66,7 @@ export const DealTicketEstimates = ({
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle> <DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
<ValueTooltipRow <ValueTooltipRow
value={estMargin} value={estMargin}
description={constants.EST_MARGIN_TOOLTIP_TEXT} description={constants.EST_MARGIN_TOOLTIP_TEXT(quoteName || '')}
/> />
</div> </div>
)} )}
@ -75,7 +75,7 @@ export const DealTicketEstimates = ({
<DataTitle quoteName={quoteName}>{t('Est. Close out')}</DataTitle> <DataTitle quoteName={quoteName}>{t('Est. Close out')}</DataTitle>
<ValueTooltipRow <ValueTooltipRow
value={estCloseOut} value={estCloseOut}
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT} description={constants.EST_CLOSEOUT_TOOLTIP_TEXT(quoteName || '')}
/> />
</div> </div>
)} )}

View File

@ -9,6 +9,7 @@ import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/react-helpers'; import { createDocsLinks } from '@vegaprotocol/react-helpers';
import { compileGridData } from './compile-grid-data'; import { compileGridData } from './compile-grid-data';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list'; import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
import BigNumber from 'bignumber.js';
type TradingModeTooltipProps = { type TradingModeTooltipProps = {
marketId?: string; marketId?: string;
@ -115,13 +116,21 @@ export const TradingModeTooltip = ({
case Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION: { case Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION: {
switch (trigger) { switch (trigger) {
case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY: { case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY: {
const notEnoughLiquidity = new BigNumber(
marketData.suppliedStake || 0
).isLessThan(marketData.targetStake || 0);
return ( return (
<section data-testid="trading-mode-tooltip"> <section data-testid="trading-mode-tooltip">
<p className={classNames({ 'mb-4': Boolean(compiledGrid) })}> <p className={classNames({ 'mb-4': Boolean(compiledGrid) })}>
<span> <span className="mb-2">
{t( {notEnoughLiquidity &&
t(
'This market is in auction until it reaches sufficient liquidity.' 'This market is in auction until it reaches sufficient liquidity.'
)} )}
{!notEnoughLiquidity &&
t(
'This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
)}
</span>{' '} </span>{' '}
{VEGA_DOCS_URL && ( {VEGA_DOCS_URL && (
<ExternalLink <ExternalLink

View File

@ -1,17 +1,27 @@
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
export const EST_MARGIN_TOOLTIP_TEXT = t( export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
'When opening a position on a futures market, you must post margin to cover any potential losses that you may incur. The margin is typically a fraction of the notional position size. For example, for a notional position size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.' t(
); `A fraction of the notional position size, in the market's settlement asset %s, to cover any potential losses that you may incur.
For example, for a notional size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.`,
[settlementAsset]
);
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t( export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t(
'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.' 'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.'
); );
export const EST_CLOSEOUT_TOOLTIP_TEXT = t( export const EST_CLOSEOUT_TOOLTIP_TEXT = (quote: string) =>
'Because you only need to post a fraction of your position size as margin when trading futures, it is possible to obtain leverage meaning your notional position size exceeds your account balance. In this scenario, if the market moves against your position, it will sometimes be necessary to force close your position due to insufficient funds. The estimated close out tells you the price at which that would happen based on current position and account balance.' t(
); `If the price drops below this number, measured in the market price quote unit %s, you will be closed out, based on your current position and account balance.`,
export const NOTIONAL_SIZE_TOOLTIP_TEXT = t( [quote]
'The notional size represents the position size in the settlement asset of the futures contract. The notional size is calculated by multiplying the number of contracts by the price of the contract. For example, ten contracts traded at a price of $50 has a notional size of $500.' );
); export const NOTIONAL_SIZE_TOOLTIP_TEXT = (settlementAsset: string) =>
t(
`The notional size represents the position size in the settlement asset %s of the futures contract. This is calculated by multiplying the number of contracts by the prices of the contract.
For example 10 contracts traded at a price of $50 has a notional size of $500.`,
[settlementAsset]
);
export const EST_FEES_TOOLTIP_TEXT = t( export const EST_FEES_TOOLTIP_TEXT = t(
'When you execute a new buy or sell order, you must pay a small amount of commission to the network for doing so. This fee is used to provide income to the node operates of the network and market makers who make prices on the futures market you are trading.' 'When you execute a new buy or sell order, you must pay a small amount of commission to the network for doing so. This fee is used to provide income to the node operates of the network and market makers who make prices on the futures market you are trading.'
); );

View File

@ -72,13 +72,13 @@ export const useFeeDealTicketDetails = (
return null; return null;
}, [derivedPrice, order.size, market.decimalPlaces]); }, [derivedPrice, order.size, market.decimalPlaces]);
const symbol = const assetSymbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol; market.tradableInstrument.instrument.product.settlementAsset.symbol;
return useMemo(() => { return useMemo(() => {
return { return {
market, market,
symbol, assetSymbol,
notionalSize, notionalSize,
estMargin, estMargin,
estCloseOut, estCloseOut,
@ -87,7 +87,7 @@ export const useFeeDealTicketDetails = (
}; };
}, [ }, [
market, market,
symbol, assetSymbol,
notionalSize, notionalSize,
estMargin, estMargin,
estCloseOut, estCloseOut,
@ -98,7 +98,7 @@ export const useFeeDealTicketDetails = (
export interface FeeDetails { export interface FeeDetails {
market: Market; market: Market;
symbol: string; assetSymbol: string;
notionalSize: string | null; notionalSize: string | null;
estMargin: OrderMargin | null; estMargin: OrderMargin | null;
estCloseOut: string | null; estCloseOut: string | null;
@ -106,7 +106,7 @@ export interface FeeDetails {
} }
export const getFeeDetailsValues = ({ export const getFeeDetailsValues = ({
symbol, assetSymbol,
notionalSize, notionalSize,
estMargin, estMargin,
estCloseOut, estCloseOut,
@ -114,6 +114,7 @@ export const getFeeDetailsValues = ({
}: FeeDetails) => { }: FeeDetails) => {
const assetDecimals = const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals; market.tradableInstrument.instrument.product.settlementAsset.decimals;
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const formatValueWithMarketDp = ( const formatValueWithMarketDp = (
value: string | number | null | undefined value: string | number | null | undefined
): string => { ): string => {
@ -132,8 +133,8 @@ export const getFeeDetailsValues = ({
{ {
label: t('Notional'), label: t('Notional'),
value: formatValueWithMarketDp(notionalSize), value: formatValueWithMarketDp(notionalSize),
symbol, symbol: assetSymbol,
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT, labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol),
}, },
{ {
label: t('Fees'), label: t('Fees'),
@ -144,31 +145,31 @@ export const getFeeDetailsValues = ({
<> <>
<span> <span>
{t( {t(
'The most you would be expected to pay in fees, the actual amount may vary.' `An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)} )}
</span> </span>
<FeesBreakdown <FeesBreakdown
fees={estMargin?.fees} fees={estMargin?.fees}
feeFactors={market.fees.factors} feeFactors={market.fees.factors}
symbol={symbol} symbol={assetSymbol}
decimals={assetDecimals} decimals={assetDecimals}
/> />
</> </>
), ),
symbol, symbol: assetSymbol,
}, },
{ {
label: t('Margin'), label: t('Margin'),
value: value:
estMargin?.margin && `~${formatValueWithAssetDp(estMargin?.margin)}`, estMargin?.margin && `~${formatValueWithAssetDp(estMargin?.margin)}`,
symbol, symbol: assetSymbol,
labelDescription: EST_MARGIN_TOOLTIP_TEXT, labelDescription: EST_MARGIN_TOOLTIP_TEXT(assetSymbol),
}, },
{ {
label: t('Liquidation'), label: t('Liquidation'),
value: estCloseOut && `~${formatValueWithMarketDp(estCloseOut)}`, value: estCloseOut && `~${formatValueWithMarketDp(estCloseOut)}`,
symbol: market.tradableInstrument.instrument.product.quoteName, symbol: market.tradableInstrument.instrument.product.quoteName,
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT, labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT(quoteName),
}, },
]; ];
}; };

View File

@ -93,7 +93,9 @@ export const Info = ({ market, onSelect }: InfoProps) => {
const headerClassName = 'uppercase text-lg'; const headerClassName = 'uppercase text-lg';
const dayVolume = calcCandleVolume(market); const dayVolume = calcCandleVolume(market);
const assetSymbol = const assetSymbol =
market?.tradableInstrument.instrument.product?.settlementAsset.symbol; market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
const assetId = useMemo( const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id, () => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market] [market]
@ -129,16 +131,27 @@ export const Info = ({ market, onSelect }: InfoProps) => {
{ {
title: t('Market price'), title: t('Market price'),
content: ( content: (
<>
<MarketInfoTable <MarketInfoTable
data={pick( data={{
...pick(
market.data, market.data,
'name', 'name',
'markPrice', 'markPrice',
'bestBidPrice', 'bestBidPrice',
'bestOfferPrice' 'bestOfferPrice'
)} ),
quoteUnit: market.tradableInstrument.instrument.product.quoteName,
}}
decimalPlaces={market.decimalPlaces} decimalPlaces={market.decimalPlaces}
/> />
<p className="text-xs">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
[assetSymbol, quoteUnit]
)}
</p>
</>
), ),
}, },
{ {
@ -189,6 +202,10 @@ export const Info = ({ market, onSelect }: InfoProps) => {
const assetDecimals = const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals; market.tradableInstrument.instrument.product.settlementAsset.decimals;
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const marketSpecPanels = [ const marketSpecPanels = [
{ {
title: t('Key details'), title: t('Key details'),
@ -224,6 +241,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
{ {
title: t('Settlement asset'), title: t('Settlement asset'),
content: asset ? ( content: asset ? (
<>
<AssetDetailsTable <AssetDetailsTable
asset={asset} asset={asset}
inline={true} inline={true}
@ -231,6 +249,13 @@ export const Info = ({ market, onSelect }: InfoProps) => {
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal" dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full" ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
/> />
<p className="text-xs">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
[assetSymbol, quoteUnit]
)}
</p>
</>
) : ( ) : (
<Splash>{t('No data')}</Splash> <Splash>{t('No data')}</Splash>
), ),
@ -342,12 +367,20 @@ export const Info = ({ market, onSelect }: InfoProps) => {
{ {
title: t('Liquidity price range'), title: t('Liquidity price range'),
content: ( content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders count towards a commitment they have to be
within either the liquidity or price monitoring bounds (whichever is
tighter).`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
price.`}
</p>
<MarketInfoTable <MarketInfoTable
data={{ data={{
liquidityPriceRange: formatNumberPercentage( liquidityPriceRange: `${liquidityPriceRange} of mid price`,
new BigNumber(market.lpPriceRange).times(100) lowestPrice:
),
LPVolumeMin:
market.data?.midPrice && market.data?.midPrice &&
`${addDecimalsFormatNumber( `${addDecimalsFormatNumber(
new BigNumber(1) new BigNumber(1)
@ -355,8 +388,8 @@ export const Info = ({ market, onSelect }: InfoProps) => {
.times(market.data.midPrice) .times(market.data.midPrice)
.toString(), .toString(),
market.decimalPlaces market.decimalPlaces
)} ${assetSymbol}`, )} ${quoteUnit}`,
LPVolumeMax: highestPrice:
market.data?.midPrice && market.data?.midPrice &&
`${addDecimalsFormatNumber( `${addDecimalsFormatNumber(
new BigNumber(1) new BigNumber(1)
@ -364,9 +397,10 @@ export const Info = ({ market, onSelect }: InfoProps) => {
.times(market.data.midPrice) .times(market.data.midPrice)
.toString(), .toString(),
market.decimalPlaces market.decimalPlaces
)} ${assetSymbol}`, )} ${quoteUnit}`,
}} }}
></MarketInfoTable> ></MarketInfoTable>
</>
), ),
}, },
{ {

View File

@ -16,6 +16,9 @@ export const tooltipMapping: Record<string, ReactNode> = {
markPrice: t( markPrice: t(
'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.'
), ),
quoteUnit: t(
`The underlying that is being priced by the market, described by the market's oracle.`
),
openInterest: t( openInterest: t(
'The volume of all open positions in a given market (the sum of the size of all positions greater than 0).' 'The volume of all open positions in a given market (the sum of the size of all positions greater than 0).'
), ),
@ -98,7 +101,4 @@ export const tooltipMapping: Record<string, ReactNode> = {
`The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.` `The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.`
), ),
suppliedStake: t('The current amount of liquidity supplied for this market.'), suppliedStake: t('The current amount of liquidity supplied for this market.'),
liquidityPriceRange: t(
`Percentage move up and down from the mid price, which specifies the range of price levels over which automated liquidity provision orders will be deployed.`
),
}; };