feat: 991 deal ticket should show estimated crossing price for market orders when market is in auction (#1561)

* feat: #991 move order validation back in deal ticket, add tooltip

* fix: align view full market list link styling

* feat: #991 move tooltip add for monitoring liq auction

* feat: #991 order validation messages, LP table sortable, padding sparkline, warning instead of errro

* feat: #991 check only in monitoring auction to add tooltips, re-use isMarketInAuction

* fix: #991 fix import for isMarketInAuction

* fix: #991 console-lite imports

* fix: formatting in console-lite deal-ticket-steps.tsx

* fix: add market depth

* fix: add market depth and fix generate orders

* fix: revert market list and generate orders

* fix: fix trading-e2e build

* feat: #991 in monitoring auction don't show a price input color update based on intent

* fix: #991 update docs links to docs.vega.xyz in tooltips
This commit is contained in:
m.ray 2022-10-03 09:41:34 +01:00 committed by GitHub
parent b10844147e
commit f743828cb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 503 additions and 269 deletions

View File

@ -1,8 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper'; import { Stepper } from '../stepper';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket'; import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import {
getDefaultOrder,
useOrderValidation,
validateSize,
} from '@vegaprotocol/deal-ticket';
import { InputError } from '@vegaprotocol/ui-toolkit'; import { InputError } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js'; import { BigNumber } from 'bignumber.js';
import { MarketSelector } from '@vegaprotocol/deal-ticket'; import { MarketSelector } from '@vegaprotocol/deal-ticket';
@ -15,14 +20,11 @@ import {
removeDecimal, removeDecimal,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { import {
getDefaultOrder,
useOrderValidation,
useOrderSubmit, useOrderSubmit,
getOrderDialogTitle, getOrderDialogTitle,
getOrderDialogIntent, getOrderDialogIntent,
getOrderDialogIcon, getOrderDialogIcon,
OrderFeedback, OrderFeedback,
validateSize,
} from '@vegaprotocol/orders'; } from '@vegaprotocol/orders';
import { DealTicketSize } from './deal-ticket-size'; import { DealTicketSize } from './deal-ticket-size';
import MarketNameRenderer from '../simple-market-list/simple-market-renderer'; import MarketNameRenderer from '../simple-market-list/simple-market-renderer';

View File

@ -28,6 +28,7 @@ export const generateDealTicketQuery = (
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC', symbol: 'tBTC',
name: 'tBTC TEST', name: 'tBTC TEST',
decimals: 5,
}, },
}, },
}, },

View File

@ -75,6 +75,13 @@ export const generateMarket = (override?: PartialDeep<Market>): Market => {
close: null, close: null,
__typename: 'MarketTimestamps', __typename: 'MarketTimestamps',
}, },
depth: {
__typename: 'MarketDepth',
lastTrade: {
__typename: 'Trade',
price: '88470230',
},
},
candlesConnection: { candlesConnection: {
__typename: 'CandleDataConnection', __typename: 'CandleDataConnection',
edges: [ edges: [

View File

@ -1 +0,0 @@
export * from './trading-mode-tooltip';

View File

@ -1,207 +0,0 @@
import type { ReactNode } from 'react';
import {
t,
getDateTimeFormat,
addDecimalsFormatNumber,
} from '@vegaprotocol/react-helpers';
import { ExternalLink, Link as UiToolkitLink } from '@vegaprotocol/ui-toolkit';
import Link from 'next/link';
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
import type { Market_market } from '../../pages/markets/__generated__/Market';
type MarketDataGridProps = {
grid: {
label: string | ReactNode;
value?: ReactNode;
}[];
};
const MarketDataGrid = ({ grid }: MarketDataGridProps) => {
return (
<>
{grid.map(
({ label, value }, index) =>
value && (
<div key={index} className="grid grid-cols-2">
<span data-testid="tooltip-label">{label}</span>
<span data-testid="tooltip-value" className="text-right">
{value}
</span>
</div>
)
)}
</>
);
};
const formatStake = (value: string, market: Market_market) => {
const formattedValue = addDecimalsFormatNumber(
value,
market.tradableInstrument.instrument.product.settlementAsset.decimals
);
const asset =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
return `${formattedValue} ${asset}`;
};
const compileGridData = (
market: Market_market,
onSelect?: (id: string) => void
) => {
const grid: MarketDataGridProps['grid'] = [];
const isLiquidityMonitoringAuction =
market.tradingMode === MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data?.trigger === AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
if (!market.data) return grid;
if (market.data?.auctionStart) {
grid.push({
label: t('Auction start'),
value: getDateTimeFormat().format(new Date(market.data.auctionStart)),
});
}
if (market.data?.auctionEnd) {
const endDate = getDateTimeFormat().format(
new Date(market.data.auctionEnd)
);
grid.push({
label: isLiquidityMonitoringAuction
? t('Est auction end')
: t('Auction end'),
value: isLiquidityMonitoringAuction ? `~${endDate}` : endDate,
});
}
if (isLiquidityMonitoringAuction && market.data?.targetStake) {
grid.push({
label: t('Target liquidity'),
value: formatStake(market.data.targetStake, market),
});
}
if (isLiquidityMonitoringAuction && market.data?.suppliedStake) {
grid.push({
label: (
<Link href={`/liquidity/${market.id}`} passHref={true}>
<UiToolkitLink onClick={() => onSelect && onSelect(market.id)}>
{t('Current liquidity')}
</UiToolkitLink>
</Link>
),
value: formatStake(market.data.suppliedStake, market),
});
}
if (market.data?.indicativePrice) {
grid.push({
label: t('Est uncrossing price'),
value:
'~' +
addDecimalsFormatNumber(
market.data.indicativePrice,
market.positionDecimalPlaces
),
});
}
if (market.data?.indicativeVolume) {
grid.push({
label: t('Est uncrossing vol'),
value:
'~' +
addDecimalsFormatNumber(
market.data.indicativeVolume,
market.positionDecimalPlaces
),
});
}
return grid;
};
type TradingModeTooltipProps = {
market: Market_market;
onSelect?: (marketId: string) => void;
};
export const TradingModeTooltip = ({
market,
onSelect,
}: TradingModeTooltipProps) => {
switch (market.tradingMode) {
case MarketTradingMode.TRADING_MODE_CONTINUOUS: {
return (
<>
{t(
'This is the standard trading mode where trades are executed whenever orders are received.'
)}
</>
);
}
case MarketTradingMode.TRADING_MODE_OPENING_AUCTION: {
return (
<>
<p className="mb-4">
<span>
{t(
'This new market is in an opening auction to determine a fair mid-price before starting continuous trading.'
)}
</span>{' '}
<ExternalLink href="https://docs.fairground.vega.xyz/docs/trading-questions/#auctions-what-happens-in-an-opening-auction">
{t('Find out more')}
</ExternalLink>
</p>
<MarketDataGrid grid={compileGridData(market)} />
</>
);
}
case MarketTradingMode.TRADING_MODE_MONITORING_AUCTION: {
switch (market.data?.trigger) {
case AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY: {
return (
<>
<p data-testid="tooltip-market-info" className="mb-4">
<span>
{t(
'This market is in auction until it reaches sufficient liquidity.'
)}
</span>{' '}
<ExternalLink href="https://docs.fairground.vega.xyz/docs/trading-questions/#auctions-what-is-a-liquidity-monitoring-auction">
{t('Find out more')}
</ExternalLink>
</p>
<MarketDataGrid grid={compileGridData(market, onSelect)} />
</>
);
}
case AuctionTrigger.AUCTION_TRIGGER_PRICE: {
return (
<>
<p className="mb-4">
<span>
{t('This market is in auction due to high price volatility.')}
</span>{' '}
<ExternalLink href="https://docs.fairground.vega.xyz/docs/trading-questions/#auctions-what-is-a-price-monitoring-auction">
{t('Find out more')}
</ExternalLink>
</p>
<MarketDataGrid grid={compileGridData(market)} />
</>
);
}
default: {
return null;
}
}
}
case MarketTradingMode.TRADING_MODE_NO_TRADING: {
return <>{t('No trading enabled for this market.')}</>;
}
case MarketTradingMode.TRADING_MODE_BATCH_AUCTION:
default: {
return null;
}
}
};

View File

@ -50,6 +50,11 @@ query Market($marketId: ID!, $interval: Interval!, $since: String!) {
open open
close close
} }
depth {
lastTrade {
price
}
}
candlesConnection(interval: $interval, since: $since) { candlesConnection(interval: $interval, since: $since) {
edges { edges {
node { node {

View File

@ -69,6 +69,11 @@ const MARKET_QUERY = gql`
open open
close close
} }
depth {
lastTrade {
price
}
}
candlesConnection(interval: $interval, since: $since) { candlesConnection(interval: $interval, since: $since) {
edges { edges {
node { node {

View File

@ -169,6 +169,22 @@ export interface Market_market_marketTimestamps {
close: string | null; close: string | null;
} }
export interface Market_market_depth_lastTrade {
__typename: "Trade";
/**
* The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64)
*/
price: string;
}
export interface Market_market_depth {
__typename: "MarketDepth";
/**
* Last trade for the given market (if available)
*/
lastTrade: Market_market_depth_lastTrade | null;
}
export interface Market_market_candlesConnection_edges_node { export interface Market_market_candlesConnection_edges_node {
__typename: "Candle"; __typename: "Candle";
/** /**
@ -251,6 +267,10 @@ export interface Market_market {
* Timestamps for state changes in the market * Timestamps for state changes in the market
*/ */
marketTimestamps: Market_market_marketTimestamps; marketTimestamps: Market_market_marketTimestamps;
/**
* Current depth on the order book for this market
*/
depth: Market_market_depth;
/** /**
* Candles on a market, for the 'last' n candles, at 'interval' seconds as specified by parameters using cursor based pagination * Candles on a market, for the 'last' n candles, at 'interval' seconds as specified by parameters using cursor based pagination
*/ */

View File

@ -10,7 +10,7 @@ export type MarketQueryVariables = Types.Exact<{
}>; }>;
export type MarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradingMode: Types.MarketTradingMode, state: Types.MarketState, decimalPlaces: number, positionDecimalPlaces: number, data?: { __typename?: 'MarketData', auctionStart?: string | null, auctionEnd?: string | null, markPrice: string, indicativeVolume: string, indicativePrice: string, suppliedStake?: string | null, targetStake?: string | null, bestBidVolume: string, bestOfferVolume: string, bestStaticBidVolume: string, bestStaticOfferVolume: string, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, oracleSpecForTradingTermination: { __typename?: 'OracleSpec', id: string }, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open?: string | null, close?: string | null }, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', open: string, close: string, volume: string } } | null> | null } | null } | null }; export type MarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradingMode: Types.MarketTradingMode, state: Types.MarketState, decimalPlaces: number, positionDecimalPlaces: number, data?: { __typename?: 'MarketData', auctionStart?: string | null, auctionEnd?: string | null, markPrice: string, indicativeVolume: string, indicativePrice: string, suppliedStake?: string | null, targetStake?: string | null, bestBidVolume: string, bestOfferVolume: string, bestStaticBidVolume: string, bestStaticOfferVolume: string, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, oracleSpecForTradingTermination: { __typename?: 'OracleSpec', id: string }, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open?: string | null, close?: string | null }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null }, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', open: string, close: string, volume: string } } | null> | null } | null } | null };
export const MarketDocument = gql` export const MarketDocument = gql`
@ -66,6 +66,11 @@ export const MarketDocument = gql`
open open
close close
} }
depth {
lastTrade {
price
}
}
candlesConnection(interval: $interval, since: $since) { candlesConnection(interval: $interval, since: $since) {
edges { edges {
node { node {

View File

@ -1,4 +1,8 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket'; import {
compileGridData,
DealTicketContainer,
TradingModeTooltip,
} from '@vegaprotocol/deal-ticket';
import { MarketInfoContainer } from '@vegaprotocol/market-info'; import { MarketInfoContainer } from '@vegaprotocol/market-info';
import { OrderbookContainer } from '@vegaprotocol/market-depth'; import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { ColumnKind, SelectMarketPopover } from '@vegaprotocol/market-list'; import { ColumnKind, SelectMarketPopover } from '@vegaprotocol/market-list';
@ -38,7 +42,6 @@ import {
MarketTradingMode, MarketTradingMode,
MarketTradingModeMapping, MarketTradingModeMapping,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { TradingModeTooltip } from '../../components/trading-mode-tooltip';
import { Header, HeaderStat } from '../../components/header'; import { Header, HeaderStat } from '../../components/header';
import { AccountsContainer } from '../portfolio/accounts-container'; import { AccountsContainer } from '../portfolio/accounts-container';
@ -173,9 +176,7 @@ export const TradeMarketHeader = ({
description={ description={
<TradingModeTooltip <TradingModeTooltip
market={market} market={market}
onSelect={(marketId: string) => { compiledGrid={compileGridData(market, onSelect)}
onSelect(marketId);
}}
/> />
} }
> >

View File

@ -1,5 +1,4 @@
import { toDecimal } from '@vegaprotocol/react-helpers'; import { toDecimal } from '@vegaprotocol/react-helpers';
import type { Order } from '../order-hooks';
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types'; import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
export const getDefaultOrder = (market: { export const getDefaultOrder = (market: {
@ -12,3 +11,13 @@ export const getDefaultOrder = (market: {
timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC,
size: String(toDecimal(market.positionDecimalPlaces)), size: String(toDecimal(market.positionDecimalPlaces)),
}); });
export interface Order {
marketId: string;
type: OrderType;
size: string;
side: Side;
timeInForce: OrderTimeInForce;
price?: string;
expiresAt?: Date;
}

View File

@ -1,2 +1,3 @@
export * from './get-default-order'; export * from './get-default-order';
export * from './use-order-validation';
export * from './validate-size'; export * from './validate-size';

View File

@ -14,12 +14,11 @@ import {
import type { ValidationProps } from './use-order-validation'; import type { ValidationProps } from './use-order-validation';
import { marketTranslations } from './use-order-validation'; import { marketTranslations } from './use-order-validation';
import { useOrderValidation } from './use-order-validation'; import { useOrderValidation } from './use-order-validation';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size'; import { ERROR_SIZE_DECIMAL } from './validate-size';
jest.mock('@vegaprotocol/wallet'); jest.mock('@vegaprotocol/wallet');
const market = { const market = {
__typename: 'Market',
id: 'market-id', id: 'market-id',
decimalPlaces: 2, decimalPlaces: 2,
positionDecimalPlaces: 1, positionDecimalPlaces: 1,
@ -74,7 +73,7 @@ const ERROR = {
MARKET_CONTINUOUS_LIMIT: MARKET_CONTINUOUS_LIMIT:
'Only limit orders are permitted when market is in auction', 'Only limit orders are permitted when market is in auction',
MARKET_CONTINUOUS_TIF: MARKET_CONTINUOUS_TIF:
'Only GTT, GTC and GFA are permitted when market is in auction', 'Until the auction ends, you can only place GFA, GTT, or GTC limit orders',
FIELD_SIZE_REQ: 'You need to provide an amount', FIELD_SIZE_REQ: 'You need to provide an amount',
FIELD_SIZE_MIN: `The amount cannot be lower than "${defaultOrder.step}"`, FIELD_SIZE_MIN: `The amount cannot be lower than "${defaultOrder.step}"`,
FIELD_PRICE_REQ: 'You need to provide a price', FIELD_PRICE_REQ: 'You need to provide a price',
@ -165,10 +164,8 @@ describe('useOrderValidation', () => {
market: { ...defaultOrder.market, tradingMode }, market: { ...defaultOrder.market, tradingMode },
orderType: OrderType.TYPE_MARKET, orderType: OrderType.TYPE_MARKET,
}); });
expect(result.current).toStrictEqual({ expect(result.current.isDisabled).toBeTruthy();
isDisabled: true, expect(result.current.message).toBe(errorMessage);
message: errorMessage,
});
} }
); );

View File

@ -3,22 +3,31 @@ import { useMemo } from 'react';
import { t, toDecimal } from '@vegaprotocol/react-helpers'; import { t, toDecimal } from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import {
AuctionTrigger,
MarketState, MarketState,
MarketStateMapping, MarketStateMapping,
MarketTradingMode, MarketTradingMode,
OrderTimeInForce, OrderTimeInForce,
OrderType, OrderType,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size'; import { Tooltip } from '@vegaprotocol/ui-toolkit';
import type { Order } from './use-order-submit'; import type { Order } from './get-default-order';
import { ERROR_SIZE_DECIMAL } from './validate-size';
import { MarketDataGrid } from '../trading-mode-tooltip';
import { compileGridData } from '../trading-mode-tooltip/compile-grid-data';
import type { DealTicketMarketFragment } from '../deal-ticket/__generated__/DealTicket';
export const isMarketInAuction = (market: DealTicketMarketFragment) => {
return [
MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
].includes(market.tradingMode);
};
export type ValidationProps = { export type ValidationProps = {
step?: number; step?: number;
market: { market: DealTicketMarketFragment;
state: MarketState;
tradingMode: MarketTradingMode;
positionDecimalPlaces: number;
};
orderType: OrderType; orderType: OrderType;
orderTimeInForce: OrderTimeInForce; orderTimeInForce: OrderTimeInForce;
fieldErrors?: FieldErrors<Order>; fieldErrors?: FieldErrors<Order>;
@ -38,7 +47,10 @@ export const useOrderValidation = ({
fieldErrors = {}, fieldErrors = {},
orderType, orderType,
orderTimeInForce, orderTimeInForce,
}: ValidationProps) => { }: ValidationProps): {
message: React.ReactNode | string;
isDisabled: boolean;
} => {
const { keypair } = useVegaWallet(); const { keypair } = useVegaWallet();
const minSize = toDecimal(market.positionDecimalPlaces); const minSize = toDecimal(market.positionDecimalPlaces);
@ -88,14 +100,54 @@ export const useOrderValidation = ({
}; };
} }
if ( if (isMarketInAuction(market)) {
[ if (orderType === OrderType.TYPE_MARKET) {
MarketTradingMode.TRADING_MODE_BATCH_AUCTION, if (
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, market.tradingMode ===
MarketTradingMode.TRADING_MODE_OPENING_AUCTION, MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
].includes(market.tradingMode) market.data?.trigger === AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
) { ) {
if (orderType !== OrderType.TYPE_LIMIT) { return {
isDisabled: true,
message: (
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
{t('Only limit orders are permitted when market is in auction')}
</span>
),
};
}
if (
market.tradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data?.trigger === AuctionTrigger.AUCTION_TRIGGER_PRICE
) {
return {
isDisabled: true,
message: (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}
{t('Only limit orders are permitted when market is in auction')}
</span>
),
};
}
return { return {
isDisabled: true, isDisabled: true,
message: t( message: t(
@ -103,18 +155,68 @@ export const useOrderValidation = ({
), ),
}; };
} }
if ( if (
orderType === OrderType.TYPE_LIMIT &&
[ [
OrderTimeInForce.TIME_IN_FORCE_FOK, OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC, OrderTimeInForce.TIME_IN_FORCE_IOC,
OrderTimeInForce.TIME_IN_FORCE_GFN, OrderTimeInForce.TIME_IN_FORCE_GFN,
].includes(orderTimeInForce) ].includes(orderTimeInForce)
) { ) {
if (
market.tradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data?.trigger === AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
) {
return {
isDisabled: true,
message: (
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
{t(
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
)}
</span>
),
};
}
if (
market.tradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data?.trigger === AuctionTrigger.AUCTION_TRIGGER_PRICE
) {
return {
isDisabled: true,
message: (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}
{t(
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
)}
</span>
),
};
}
return { return {
isDisabled: true, isDisabled: true,
message: t( message: t(
'Only GTT, GTC and GFA are permitted when market is in auction' `Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
), ),
}; };
} }

View File

@ -4,6 +4,18 @@ fragment DealTicketMarket on Market {
positionDecimalPlaces positionDecimalPlaces
state state
tradingMode tradingMode
data {
market {
id
}
indicativePrice
indicativeVolume
targetStake
suppliedStake
auctionStart
auctionEnd
trigger
}
tradableInstrument { tradableInstrument {
instrument { instrument {
id id
@ -14,6 +26,7 @@ fragment DealTicketMarket on Market {
settlementAsset { settlementAsset {
id id
symbol symbol
decimals
name name
} }
} }

View File

@ -3,14 +3,14 @@ import { Schema 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 DealTicketMarketFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } }; export type DealTicketMarketFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, data?: { __typename?: 'MarketData', indicativePrice: string, indicativeVolume: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } };
export type DealTicketQueryVariables = Types.Exact<{ export type DealTicketQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketId: Types.Scalars['ID'];
}>; }>;
export type DealTicketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } } | null }; export type DealTicketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, data?: { __typename?: 'MarketData', indicativePrice: string, indicativeVolume: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } } | null };
export const DealTicketMarketFragmentDoc = gql` export const DealTicketMarketFragmentDoc = gql`
fragment DealTicketMarket on Market { fragment DealTicketMarket on Market {
@ -19,6 +19,18 @@ export const DealTicketMarketFragmentDoc = gql`
positionDecimalPlaces positionDecimalPlaces
state state
tradingMode tradingMode
data {
market {
id
}
indicativePrice
indicativeVolume
targetStake
suppliedStake
auctionStart
auctionEnd
trigger
}
tradableInstrument { tradableInstrument {
instrument { instrument {
id id
@ -29,6 +41,7 @@ export const DealTicketMarketFragmentDoc = gql`
settlementAsset { settlementAsset {
id id
symbol symbol
decimals
name name
} }
} }

View File

@ -1,7 +1,7 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit'; import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import { t, toDecimal } from '@vegaprotocol/react-helpers'; import { t, toDecimal } from '@vegaprotocol/react-helpers';
import { validateSize } from '@vegaprotocol/orders';
import type { DealTicketAmountProps } from './deal-ticket-amount'; import type { DealTicketAmountProps } from './deal-ticket-amount';
import { validateSize } from '../deal-ticket-validation';
export type DealTicketLimitAmountProps = Omit< export type DealTicketLimitAmountProps = Omit<
DealTicketAmountProps, DealTicketAmountProps,

View File

@ -1,7 +1,8 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit'; import { FormGroup, Input, Tooltip } from '@vegaprotocol/ui-toolkit';
import { t, toDecimal } from '@vegaprotocol/react-helpers'; import { t, toDecimal } from '@vegaprotocol/react-helpers';
import { validateSize } from '@vegaprotocol/orders';
import type { DealTicketAmountProps } from './deal-ticket-amount'; import type { DealTicketAmountProps } from './deal-ticket-amount';
import { validateSize } from '../deal-ticket-validation/validate-size';
import { isMarketInAuction } from '../deal-ticket-validation/use-order-validation';
export type DealTicketMarketAmountProps = Omit< export type DealTicketMarketAmountProps = Omit<
DealTicketAmountProps, DealTicketAmountProps,
@ -37,13 +38,26 @@ export const DealTicketMarketAmount = ({
</div> </div>
<div>@</div> <div>@</div>
<div className="flex-1" data-testid="last-price"> <div className="flex-1" data-testid="last-price">
{price && quoteName ? ( {isMarketInAuction(market) && (
<> <Tooltip
~{price} {quoteName} description={t(
</> 'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
) : ( )}
'-' >
<span className={'block mb-2 text-sm text-left'}>
{t(`Estimated uncrossing price`)}
</span>
</Tooltip>
)} )}
<span className="text-sm">
{price && quoteName ? (
<>
~{price} {quoteName}
</>
) : (
'-'
)}
</span>
</div> </div>
</div> </div>
); );

View File

@ -27,6 +27,7 @@ const market: DealTicketMarketFragment = {
id: 'asset-id', id: 'asset-id',
name: 'asset-name', name: 'asset-name',
symbol: 'asset-symbol', symbol: 'asset-symbol',
decimals: 2,
}, },
}, },
}, },

View File

@ -12,9 +12,11 @@ import { DealTicketAmount } from './deal-ticket-amount';
import { TimeInForceSelector } from './time-in-force-selector'; import { TimeInForceSelector } from './time-in-force-selector';
import type { DealTicketMarketFragment } from './__generated__/DealTicket'; import type { DealTicketMarketFragment } from './__generated__/DealTicket';
import { ExpirySelector } from './expiry-selector'; import { ExpirySelector } from './expiry-selector';
import type { Order } from '@vegaprotocol/orders';
import { getDefaultOrder, useOrderValidation } from '@vegaprotocol/orders';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types'; import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import type { Order } from '../deal-ticket-validation';
import { getDefaultOrder } from '../deal-ticket-validation';
import { useOrderValidation } from '../deal-ticket-validation/use-order-validation';
import { MarketTradingMode } from '@vegaprotocol/types';
export type TransactionStatus = 'default' | 'pending'; export type TransactionStatus = 'default' | 'pending';
@ -70,6 +72,22 @@ export const DealTicket = ({
[isDisabled, submit, market.decimalPlaces, market.positionDecimalPlaces] [isDisabled, submit, market.decimalPlaces, market.positionDecimalPlaces]
); );
const getPrice = () => {
if (
market.tradingMode === MarketTradingMode.TRADING_MODE_OPENING_AUCTION ||
market.tradingMode === MarketTradingMode.TRADING_MODE_BATCH_AUCTION
) {
return market.data?.indicativePrice;
}
if (
market.tradingMode === MarketTradingMode.TRADING_MODE_MONITORING_AUCTION
) {
return null;
}
return market.depth.lastTrade?.price;
};
const price = getPrice();
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate> <form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate>
<Controller <Controller
@ -101,11 +119,8 @@ export const DealTicket = ({
market={market} market={market}
register={register} register={register}
price={ price={
market.depth.lastTrade price
? addDecimalsFormatNumber( ? addDecimalsFormatNumber(price, market.decimalPlaces)
market.depth.lastTrade.price,
market.decimalPlaces
)
: undefined : undefined
} }
quoteName={market.tradableInstrument.instrument.product.quoteName} quoteName={market.tradableInstrument.instrument.product.quoteName}

View File

@ -1 +1,3 @@
export * from './deal-ticket'; export * from './deal-ticket';
export * from './deal-ticket-validation';
export * from './trading-mode-tooltip';

View File

@ -0,0 +1,98 @@
import {
t,
getDateTimeFormat,
addDecimalsFormatNumber,
} from '@vegaprotocol/react-helpers';
import { Link as UiToolkitLink } from '@vegaprotocol/ui-toolkit';
import Link from 'next/link';
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { MarketDataGridProps } from './market-data-grid';
import type { DealTicketMarketFragment } from '../deal-ticket/__generated__/DealTicket';
export const compileGridData = (
market: DealTicketMarketFragment,
onSelect?: (id: string) => void
): { label: ReactNode; value?: ReactNode }[] => {
const grid: MarketDataGridProps['grid'] = [];
const isLiquidityMonitoringAuction =
market.tradingMode === MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data?.trigger === AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
const formatStake = (value: string) => {
const formattedValue = addDecimalsFormatNumber(
value,
market.tradableInstrument.instrument.product.settlementAsset.decimals
);
const asset =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
return `${formattedValue} ${asset}`;
};
if (!market.data) return grid;
if (market.data?.auctionStart) {
grid.push({
label: t('Auction start'),
value: getDateTimeFormat().format(new Date(market.data.auctionStart)),
});
}
if (market.data?.auctionEnd) {
const endDate = getDateTimeFormat().format(
new Date(market.data.auctionEnd)
);
grid.push({
label: isLiquidityMonitoringAuction
? t('Est auction end')
: t('Auction end'),
value: isLiquidityMonitoringAuction ? `~${endDate}` : endDate,
});
}
if (isLiquidityMonitoringAuction && market.data?.targetStake) {
grid.push({
label: t('Target liquidity'),
value: formatStake(market.data.targetStake),
});
}
if (isLiquidityMonitoringAuction && market.data?.suppliedStake) {
grid.push({
label: (
<Link href={`/liquidity/${market.id}`} passHref={true}>
<UiToolkitLink onClick={() => onSelect && onSelect(market.id)}>
{t('Current liquidity')}
</UiToolkitLink>
</Link>
),
value: formatStake(market.data.suppliedStake),
});
}
if (market.data?.indicativePrice) {
grid.push({
label: t('Est uncrossing price'),
value:
'~' +
addDecimalsFormatNumber(
market.data.indicativePrice,
market.positionDecimalPlaces
),
});
}
if (market.data?.indicativeVolume) {
grid.push({
label: t('Est uncrossing vol'),
value:
'~' +
addDecimalsFormatNumber(
market.data.indicativeVolume,
market.positionDecimalPlaces
),
});
}
return grid;
};

View File

@ -0,0 +1,3 @@
export * from './market-data-grid';
export * from './trading-mode-tooltip';
export * from './compile-grid-data';

View File

@ -0,0 +1,26 @@
import type { ReactNode } from 'react';
export type MarketDataGridProps = {
grid: {
label: string | ReactNode;
value?: ReactNode;
}[];
};
export const MarketDataGrid = ({ grid }: MarketDataGridProps) => {
return (
<>
{grid.map(
({ label, value }, index) =>
value && (
<div key={index} className="grid grid-cols-2">
<span data-testid="tooltip-label">{label}</span>
<span data-testid="tooltip-value" className="text-right">
{value}
</span>
</div>
)
)}
</>
);
};

View File

@ -0,0 +1,93 @@
import { t } from '@vegaprotocol/react-helpers';
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import { MarketDataGrid } from './market-data-grid';
type TradingModeTooltipProps = {
market: {
tradingMode: MarketTradingMode;
data: { trigger: AuctionTrigger | null } | null;
};
compiledGrid: { label: ReactNode; value?: ReactNode }[];
};
export const TradingModeTooltip = ({
market,
compiledGrid,
}: TradingModeTooltipProps) => {
switch (market.tradingMode) {
case MarketTradingMode.TRADING_MODE_CONTINUOUS: {
return (
<>
{t(
'This is the standard trading mode where trades are executed whenever orders are received.'
)}
</>
);
}
case MarketTradingMode.TRADING_MODE_OPENING_AUCTION: {
return (
<>
<p className="mb-4">
<span>
{t(
'This new market is in an opening auction to determine a fair mid-price before starting continuous trading.'
)}
</span>{' '}
<ExternalLink href="https://docs.vega.xyz/docs/testnet/concepts/trading-on-vega/trading-modes#auction-type-opening">
{t('Find out more')}
</ExternalLink>
</p>
<MarketDataGrid grid={compiledGrid} />
</>
);
}
case MarketTradingMode.TRADING_MODE_MONITORING_AUCTION: {
switch (market.data?.trigger) {
case AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY: {
return (
<>
<p data-testid="tooltip-market-info" className="mb-4">
<span>
{t(
'This market is in auction until it reaches sufficient liquidity.'
)}
</span>{' '}
<ExternalLink href="https://docs.vega.xyz/docs/testnet/concepts/trading-on-vega/trading-modes#auction-type-liquidity-monitoring">
{t('Find out more')}
</ExternalLink>
</p>
<MarketDataGrid grid={compiledGrid} />
</>
);
}
case AuctionTrigger.AUCTION_TRIGGER_PRICE: {
return (
<>
<p className="mb-4">
<span>
{t('This market is in auction due to high price volatility.')}
</span>{' '}
<ExternalLink href="https://docs.vega.xyz/docs/testnet/concepts/trading-on-vega/trading-modes#auction-type-price-monitoring">
{t('Find out more')}
</ExternalLink>
</p>
<MarketDataGrid grid={compiledGrid} />
</>
);
}
default: {
return null;
}
}
}
case MarketTradingMode.TRADING_MODE_NO_TRADING: {
return <>{t('No trading enabled for this market.')}</>;
}
case MarketTradingMode.TRADING_MODE_BATCH_AUCTION:
default: {
return null;
}
}
};

View File

@ -39,7 +39,7 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
({ data, symbol = '', assetDecimalPlaces }, ref) => { ({ data, symbol = '', assetDecimalPlaces }, ref) => {
const assetDecimalsFormatter = ({ value }: ValueFormatterParams) => { const assetDecimalsFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-'; if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`; return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0, 5)}`;
}; };
return ( return (
<AgGrid <AgGrid
@ -54,6 +54,7 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
resizable: true, resizable: true,
minWidth: 100, minWidth: 100,
tooltipComponent: TooltipCellComponent, tooltipComponent: TooltipCellComponent,
sortable: true,
}} }}
rowData={data} rowData={data}
> >

View File

@ -77,7 +77,9 @@ export const SelectMarketLandingTable = ({
</tbody> </tbody>
</table> </table>
</div> </div>
<Link href="/markets">{'Or view full market list'}</Link> <div className="mt-4 text-md">
<Link href="/markets">{'Or view full market list'}</Link>
</div>
</> </>
); );
}; };

View File

@ -1,3 +1,2 @@
export * from './components'; export * from './components';
export * from './order-hooks'; export * from './order-hooks';
export * from './utils';

View File

@ -2,5 +2,4 @@ export * from './__generated__/OrderEvent';
export * from './order-event-query'; export * from './order-event-query';
export * from './use-order-cancel'; export * from './use-order-cancel';
export * from './use-order-submit'; export * from './use-order-submit';
export * from './use-order-validation';
export * from './use-order-edit'; export * from './use-order-edit';

View File

@ -3625,7 +3625,7 @@ export type QuerypartiesConnectionArgs = {
/** Queries allow a caller to read data and filter data via GraphQL. */ /** Queries allow a caller to read data and filter data via GraphQL. */
export type QuerypartyArgs = { export type QuerypartyArgs = {
id: Scalars['ID']; id?: InputMaybe<Scalars['ID']>;
}; };

View File

@ -14,11 +14,15 @@ export const InputError = ({
...props ...props
}: InputErrorProps) => { }: InputErrorProps) => {
const effectiveClassName = classNames( const effectiveClassName = classNames(
'text-sm text-vega-red flex items-center', 'text-sm flex items-center',
'mt-2', 'mt-2',
{ {
'border-danger': intent === 'danger', 'border-danger': intent === 'danger',
'border-warning': intent === 'warning', 'border-warning': intent === 'warning',
},
{
'text-warning': intent === 'warning',
'text-danger': intent === 'danger',
} }
); );
return ( return (

View File

@ -92,7 +92,10 @@ export const SparklineView = ({
return ( return (
<svg <svg
data-testid="sparkline-svg" data-testid="sparkline-svg"
className={classNames('pt-px pr-0 w-full overflow-visible', className)} className={classNames(
'pt-px pr-0 w-full overflow-visible p-2',
className
)}
width={width} width={width}
height={height} height={height}
viewBox="0 0 100 100" viewBox="0 0 100 100"

View File

@ -8,6 +8,7 @@
"test": "nx test", "test": "nx test",
"postinstall": "husky install && yarn tsc -b tools/executors/next && yarn tsc -b tools/executors/webpack", "postinstall": "husky install && yarn tsc -b tools/executors/next && yarn tsc -b tools/executors/webpack",
"test:all": "nx run-many --all --target=test", "test:all": "nx run-many --all --target=test",
"build:all": "nx run-many --all --target=build",
"vegacapsule": "vegacapsule network bootstrap --config-path=../frontend-monorepo/vegacapsule/config.hcl" "vegacapsule": "vegacapsule network bootstrap --config-path=../frontend-monorepo/vegacapsule/config.hcl"
}, },
"engines": { "engines": {