diff --git a/apps/console-lite-e2e/src/integration/market-trade.test.ts b/apps/console-lite-e2e/src/integration/market-trade.test.ts
index f4dc1dbb1..6665940bd 100644
--- a/apps/console-lite-e2e/src/integration/market-trade.test.ts
+++ b/apps/console-lite-e2e/src/integration/market-trade.test.ts
@@ -170,7 +170,50 @@ describe('Market trade', () => {
.find('button')
.should('have.text', '2');
cy.get('button').contains('Max').click();
- cy.getByTestId('price-slippage-value').should('have.text', '0.02%');
+ }
+ });
+
+ it('slippage value should be displayed', () => {
+ if (markets?.length) {
+ cy.visit(`/trading/${markets[1].id}`);
+ connectVegaWallet();
+ cy.get('#step-1-control [aria-label^="Selected value"]').click();
+ cy.get('button[aria-label="Open short position"]').click();
+ cy.get('#step-2-control').click();
+ cy.get('button').contains('Max').click();
+ cy.get('#step-2-panel')
+ .find('dl')
+ .eq(2)
+ .find('dd')
+ .should('have.text', '0.02%');
+ }
+ });
+
+ it('allow slippage value to be adjusted', () => {
+ if (markets?.length) {
+ cy.visit(`/trading/${markets[1].id}`);
+ connectVegaWallet();
+ cy.get('#step-1-control [aria-label^="Selected value"]').click();
+ cy.get('button[aria-label="Open short position"]').click();
+ cy.get('#step-2-control').click();
+ cy.get('button').contains('Max').click();
+ cy.get('#step-2-panel')
+ .find('dl')
+ .eq(2)
+ .find('dd')
+ .should('have.text', '0.02%');
+ cy.get('#step-2-panel').find('dl').eq(2).find('button').click();
+ cy.get('#input-order-slippage')
+ .focus()
+ .type('{backspace}{backspace}{backspace}1');
+
+ cy.getByTestId('slippage-dialog').find('button').click();
+
+ cy.get('#step-2-panel')
+ .find('dl')
+ .eq(2)
+ .find('dd')
+ .should('have.text', '1%');
}
});
@@ -195,9 +238,9 @@ describe('Market trade', () => {
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel')
.find('dt')
- .eq(3)
+ .eq(2)
.should('have.text', 'Est. Position Size (tDAI)');
- cy.get('#step-2-panel').find('dd').eq(3).should('have.text', '197.86012');
+ cy.get('#step-2-panel').find('dd').eq(2).should('have.text', '197.86012');
}
});
@@ -210,11 +253,11 @@ describe('Market trade', () => {
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dt')
- .eq(4)
+ .eq(3)
.should('have.text', 'Est. Fees (tDAI)');
cy.get('#step-2-panel')
.find('dd')
- .eq(4)
+ .eq(3)
.should('have.text', '3.00000 (3.03%)');
}
});
diff --git a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-estimates.tsx b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-estimates.tsx
index 97c450d29..21a842ffb 100644
--- a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-estimates.tsx
+++ b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-estimates.tsx
@@ -4,6 +4,7 @@ import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import * as constants from './constants';
+import { TrafficLight } from '../traffic-light';
interface DealTicketEstimatesProps {
quoteName?: string;
@@ -13,6 +14,7 @@ interface DealTicketEstimatesProps {
fees?: string;
notionalSize?: string;
size?: string;
+ slippage?: string;
}
interface DataTitleProps {
@@ -20,7 +22,7 @@ interface DataTitleProps {
quoteName?: string;
}
-const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
+export const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
{children}
{quoteName && ({quoteName})}
@@ -28,14 +30,20 @@ const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
);
interface ValueTooltipProps {
- value: string;
+ value?: string;
+ children?: ReactNode;
description: string;
id?: string;
}
-const ValueTooltipRow = ({ value, description, id }: ValueTooltipProps) => (
+export const ValueTooltipRow = ({
+ value,
+ children,
+ description,
+ id,
+}: ValueTooltipProps) => (
- {value}
+ {value || children}
(
{size && (
@@ -93,7 +102,7 @@ export const DealTicketEstimates = ({
)}
{estMargin && (
-
+
{t('Est. Margin')}
)}
{estCloseOut && (
-
-
- {t('Est. Close out')}
-
- ({quoteName})
-
+
+ {t('Est. Close out')}
)}
+ {slippage && (
+
+ {t('Est. Price Impact / Slippage')}
+
+
+ {slippage}%
+
+
+
+ )}
);
diff --git a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-size-input.tsx b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-size-input.tsx
new file mode 100644
index 000000000..d3b594f38
--- /dev/null
+++ b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-size-input.tsx
@@ -0,0 +1,149 @@
+import React, { useCallback, useState } from 'react';
+import { BigNumber } from 'bignumber.js';
+import { t } from '@vegaprotocol/react-helpers';
+import {
+ SliderRoot,
+ SliderThumb,
+ SliderTrack,
+ SliderRange,
+ FormGroup,
+} from '@vegaprotocol/ui-toolkit';
+import { InputSetter } from '../input-setter';
+
+interface DealTicketSizeInputProps {
+ step: number;
+ min: number;
+ max: number;
+ value: number;
+ onValueChange: (value: number) => void;
+ positionDecimalPlaces: number;
+}
+
+const getSizeLabel = (value: number): string => {
+ const MIN_LABEL = 'Min';
+ const MAX_LABEL = 'Max';
+ if (value === 0) {
+ return MIN_LABEL;
+ } else if (value === 100) {
+ return MAX_LABEL;
+ }
+
+ return `${value}%`;
+};
+
+export const DealTicketSizeInput = ({
+ value,
+ step,
+ min,
+ max,
+ onValueChange,
+ positionDecimalPlaces,
+}: DealTicketSizeInputProps) => {
+ const sizeRatios = [0, 25, 50, 75, 100];
+ const [inputValue, setInputValue] = useState(value);
+
+ const onInputValueChange = useCallback(
+ (event: React.ChangeEvent
) => {
+ let value = parseFloat(event.target.value);
+ const isLessThanMin = value < min;
+ const isMoreThanMax = value > max;
+ if (isLessThanMin) {
+ value = min;
+ } else if (isMoreThanMax) {
+ value = max;
+ }
+
+ if (value) {
+ onValueChange(value);
+ }
+
+ setInputValue(value);
+ },
+ [min, max, onValueChange, setInputValue]
+ );
+
+ const onButtonValueChange = (size: number) => {
+ const newVal = new BigNumber(size)
+ .decimalPlaces(positionDecimalPlaces)
+ .toNumber();
+ onValueChange(newVal);
+ setInputValue(newVal);
+ };
+
+ const onSliderValueChange = useCallback(
+ (value: number[]) => {
+ const val = value[0];
+ setInputValue(val);
+ onValueChange(val);
+ },
+ [onValueChange]
+ );
+
+ return (
+
+
+ {min}
+ {max}
+
+
+
+
+
+
+
+
+
+ {sizeRatios.map((size, index) => {
+ const proportionalSize = size ? (size / 100) * max : min;
+ return (
+
+ );
+ })}
+
+
+
+
+
- {t('Contracts')}
+ -
+
+
+ {inputValue}
+
+
+
+
+
+
+ );
+};
diff --git a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-size.tsx b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-size.tsx
index 7ad66a31c..b95500df0 100644
--- a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-size.tsx
+++ b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-size.tsx
@@ -1,27 +1,13 @@
-import React, { useCallback, useState } from 'react';
-import classNames from 'classnames';
-import { IconNames } from '@blueprintjs/icons';
-import { t } from '@vegaprotocol/react-helpers';
-import {
- SliderRoot,
- SliderThumb,
- SliderTrack,
- SliderRange,
- FormGroup,
- Icon,
- Tooltip,
-} from '@vegaprotocol/ui-toolkit';
-import { BigNumber } from 'bignumber.js';
+import React from 'react';
import { DealTicketEstimates } from './deal-ticket-estimates';
-import { InputSetter } from '../input-setter';
-import * as constants from './constants';
+import { DealTicketSizeInput } from './deal-ticket-size-input';
interface DealTicketSizeProps {
step: number;
min: number;
max: number;
- value: number;
- onValueChange: (value: number[]) => void;
+ size: number;
+ onSizeChange: (value: number) => void;
name: string;
quoteName: string;
price: string;
@@ -30,173 +16,33 @@ interface DealTicketSizeProps {
fees: string;
positionDecimalPlaces: number;
notionalSize: string;
- slippage: string | null;
}
-const getSizeLabel = (value: number): string => {
- const MIN_LABEL = 'Min';
- const MAX_LABEL = 'Max';
- if (value === 0) {
- return MIN_LABEL;
- } else if (value === 100) {
- return MAX_LABEL;
- }
-
- return `${value}%`;
-};
-
export const DealTicketSize = ({
- value,
step,
min,
max,
price,
quoteName,
- onValueChange,
+ size,
+ onSizeChange,
estCloseOut,
positionDecimalPlaces,
fees,
notionalSize,
- slippage,
}: DealTicketSizeProps) => {
- const sizeRatios = [0, 25, 50, 75, 100];
- const [inputValue, setInputValue] = useState(value);
-
- const onInputValueChange = useCallback(
- (event: React.ChangeEvent) => {
- const value = parseFloat(event.target.value);
- const isLessThanMin = value < min;
- const isMoreThanMax = value > max;
- if (value) {
- if (isLessThanMin) {
- onValueChange([min]);
- } else if (isMoreThanMax) {
- onValueChange([max]);
- } else {
- onValueChange([value]);
- }
- }
- setInputValue(value);
- },
- [min, max, onValueChange, setInputValue]
- );
-
- const onButtonValueChange = useCallback(
- (size: number) => {
- const newVal = new BigNumber(size)
- .decimalPlaces(positionDecimalPlaces)
- .toNumber();
- onValueChange([newVal]);
- setInputValue(newVal);
- },
- [onValueChange, positionDecimalPlaces]
- );
-
- const onSliderValueChange = useCallback(
- (value: number[]) => {
- setInputValue(value[0]);
- onValueChange(value);
- },
- [onValueChange]
- );
-
return max === 0 ? (
Not enough balance to trade
) : (
-
- {min}
- {max}
-
-
-
-
-
-
-
-
-
- {sizeRatios.map((size, index) => {
- const proportionalSize = size ? (size / 100) * max : min;
- return (
-
- );
- })}
-
-
-
-
-
- {t('Contracts')}
- -
-
-
-
-
-
-
- {slippage && (
-
-
-
- {t('Est. Price Impact / Slippage')}
-
-
- = 1 && parseFloat(slippage) < 5,
- 'text-vega-red': parseFloat(slippage) >= 5,
- })}
- >
- {slippage}%
-
-
-
-
-
-
-
-
-
- )}
+ value={size}
+ onValueChange={onSizeChange}
+ positionDecimalPlaces={positionDecimalPlaces}
+ />
{
+ const [isDialogVisible, setIsDialogVisible] = useState(false);
+
+ const onChange = useCallback(
+ (event: React.ChangeEvent) => {
+ const value = event.target.value;
+ const numericValue = parseFloat(value);
+ onValueChange(numericValue);
+ },
+ [onValueChange]
+ );
+
+ const toggleDialog = useCallback(() => {
+ setIsDialogVisible(!isDialogVisible);
+ }, [isDialogVisible]);
+
+ const formLabel = (
+
+ );
+
+ return (
+ <>
+
+
+
+
{t('Est. Price Impact / Slippage')}
+
+
+
+
+ {value}%
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx
index 18b6f7c97..078a50d76 100644
--- a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx
+++ b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper';
@@ -10,7 +10,7 @@ import type { Order } from '@vegaprotocol/orders';
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
import {
t,
- addDecimal,
+ addDecimalsFormatNumber,
toDecimal,
removeDecimal,
} from '@vegaprotocol/react-helpers';
@@ -33,6 +33,8 @@ import useOrderCloseOut from '../../hooks/use-order-closeout';
import useOrderMargin from '../../hooks/use-order-margin';
import useMaximumPositionSize from '../../hooks/use-maximum-position-size';
import useCalculateSlippage from '../../hooks/use-calculate-slippage';
+import { Side, OrderType } from '@vegaprotocol/types';
+import { DealTicketSlippage } from './deal-ticket-slippage';
interface DealTicketMarketProps {
market: DealTicketQuery_market;
@@ -62,28 +64,26 @@ export const DealTicketSteps = ({
defaultValues: getDefaultOrder(market),
});
- const [max, setMax] = useState(null);
+ const emptyString = ' - ';
const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const orderSide = watch('side');
const orderSize = watch('size');
const order = watch();
- const estCloseOut = useOrderCloseOut({ order, market, partyData });
+ const { message: invalidText, isDisabled } = useOrderValidation({
+ market,
+ orderType,
+ orderTimeInForce,
+ fieldErrors: errors,
+ });
+ const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
const { keypair } = useVegaWallet();
const estMargin = useOrderMargin({
order,
market,
partyId: keypair?.pub || '',
});
- const value = new BigNumber(orderSize).toNumber();
- const price =
- market.depth.lastTrade &&
- addDecimal(market.depth.lastTrade.price, market.decimalPlaces);
- const emptyString = ' - ';
-
- const [notionalSize, setNotionalSize] = useState(null);
- const [fees, setFees] = useState(null);
const maxTrade = useMaximumPositionSize({
partyId: keypair?.pub || '',
@@ -94,45 +94,48 @@ export const DealTicketSteps = ({
price: market?.depth?.lastTrade?.price,
order,
});
+
+ const estCloseOut = useOrderCloseOut({ order, market, partyData });
const slippage = useCalculateSlippage({ marketId: market.id, order });
- useEffect(() => {
- setMax(
- new BigNumber(maxTrade)
- .decimalPlaces(market.positionDecimalPlaces)
- .toNumber()
- );
- }, [maxTrade, market.positionDecimalPlaces]);
-
- const { message: invalidText, isDisabled } = useOrderValidation({
- market,
- orderType,
- orderTimeInForce,
- fieldErrors: errors,
- });
-
- const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
-
- const onSizeChange = (value: number[]) => {
- const newVal = new BigNumber(value[0])
- .decimalPlaces(market.positionDecimalPlaces)
- .toString();
- const isValid = validateSize(step)(newVal);
- if (isValid !== 'step') {
- setValue('size', newVal);
- }
- };
+ const [slippageValue, setSlippageValue] = useState(
+ slippage ? parseFloat(slippage) : 0
+ );
+ const transactionStatus =
+ transaction.status === VegaTxStatus.Requested ||
+ transaction.status === VegaTxStatus.Pending
+ ? 'pending'
+ : 'default';
useEffect(() => {
- if (market?.depth?.lastTrade?.price) {
- const size = new BigNumber(market.depth.lastTrade.price)
- .multipliedBy(value)
+ setSlippageValue(slippage ? parseFloat(slippage) : 0);
+ }, [slippage]);
+
+ const price = useMemo(() => {
+ if (slippage && market?.depth?.lastTrade?.price) {
+ const isLong = order.side === Side.SIDE_BUY;
+ const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
+ parseFloat(slippage) / 100
+ );
+ return new BigNumber(market?.depth?.lastTrade?.price)
+ .multipliedBy(multiplier)
.toNumber();
-
- setNotionalSize(addDecimal(size, market.decimalPlaces));
}
- }, [market, value]);
+ return null;
+ }, [market?.depth?.lastTrade?.price, order.side, slippage]);
- useEffect(() => {
+ const formattedPrice =
+ price && addDecimalsFormatNumber(price, market.decimalPlaces);
+
+ const notionalSize = useMemo(() => {
+ if (price) {
+ const size = new BigNumber(price).multipliedBy(orderSize).toNumber();
+
+ return addDecimalsFormatNumber(size, market.decimalPlaces);
+ }
+ return null;
+ }, [market.decimalPlaces, orderSize, price]);
+
+ const fees = useMemo(() => {
if (estMargin?.fees && notionalSize) {
const percentage = new BigNumber(estMargin?.fees)
.dividedBy(notionalSize)
@@ -140,17 +143,66 @@ export const DealTicketSteps = ({
.decimalPlaces(2)
.toString();
- setFees(`${estMargin.fees} (${percentage}%)`);
+ return `${estMargin.fees} (${percentage}%)`;
}
- }, [estMargin, notionalSize]);
- const transactionStatus =
- transaction.status === VegaTxStatus.Requested ||
- transaction.status === VegaTxStatus.Pending
- ? 'pending'
- : 'default';
+ return null;
+ }, [estMargin?.fees, notionalSize]);
- const onSubmit = React.useCallback(
+ const max = useMemo(() => {
+ return new BigNumber(maxTrade)
+ .decimalPlaces(market.positionDecimalPlaces)
+ .toNumber();
+ }, [market.positionDecimalPlaces, maxTrade]);
+
+ const onSizeChange = useCallback(
+ (value: number) => {
+ const newVal = new BigNumber(value)
+ .decimalPlaces(market.positionDecimalPlaces)
+ .toString();
+ const isValid = validateSize(step)(newVal);
+ if (isValid !== 'step') {
+ setValue('size', newVal);
+ }
+ },
+ [market.positionDecimalPlaces, setValue, step]
+ );
+
+ const onSlippageChange = useCallback(
+ (value: number) => {
+ if (market?.depth?.lastTrade?.price) {
+ if (value) {
+ const isLong = order.side === Side.SIDE_BUY;
+ const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
+ value / 100
+ );
+ const bestAskPrice = new BigNumber(market?.depth?.lastTrade?.price)
+ .multipliedBy(multiplier)
+ .decimalPlaces(market.decimalPlaces)
+ .toString();
+
+ setValue('price', bestAskPrice);
+
+ if (orderType === OrderType.TYPE_MARKET) {
+ setValue('type', OrderType.TYPE_LIMIT);
+ }
+ } else {
+ setValue('type', OrderType.TYPE_MARKET);
+ setValue('price', market?.depth?.lastTrade?.price);
+ }
+ }
+ setSlippageValue(value);
+ },
+ [
+ market.decimalPlaces,
+ market?.depth?.lastTrade?.price,
+ order.side,
+ orderType,
+ setValue,
+ ]
+ );
+
+ const onSubmit = useCallback(
(order: Order) => {
if (transactionStatus !== 'pending') {
submit({
@@ -198,25 +250,30 @@ export const DealTicketSteps = ({
label: t('Choose Position Size'),
component:
max !== null ? (
-
+ <>
+
+
+ >
) : (
'loading...'
),
@@ -240,13 +297,14 @@ export const DealTicketSteps = ({
order={order}
estCloseOut={estCloseOut}
estMargin={estMargin?.margin || emptyString}
- price={price || emptyString}
+ price={formattedPrice || emptyString}
quoteName={
market.tradableInstrument.instrument.product.settlementAsset
.symbol
}
notionalSize={notionalSize || emptyString}
fees={fees || emptyString}
+ slippage={slippageValue}
/>