Feat/1089 override slippage (#1283)

* feat(console-lite): add traffic light component

* fix(console-lite): add slippage to deal ticket estimates

* feat(console-lite): refactor deal ticket steps and add slippage dialog

* feat(console-lite): add e2e tests for slippage and fix broken ones

* fix(console-lite): fix styling issue after theme update
This commit is contained in:
Elmar 2022-09-08 13:04:17 +01:00 committed by GitHub
parent 65528a8007
commit 37aa5be725
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 505 additions and 262 deletions

View File

@ -170,7 +170,50 @@ describe('Market trade', () => {
.find('button') .find('button')
.should('have.text', '2'); .should('have.text', '2');
cy.get('button').contains('Max').click(); 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('dd').eq(0).find('button').click();
cy.get('#step-2-panel') cy.get('#step-2-panel')
.find('dt') .find('dt')
.eq(3) .eq(2)
.should('have.text', 'Est. Position Size (tDAI)'); .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-control').click();
cy.get('#step-2-panel') cy.get('#step-2-panel')
.find('dt') .find('dt')
.eq(4) .eq(3)
.should('have.text', 'Est. Fees (tDAI)'); .should('have.text', 'Est. Fees (tDAI)');
cy.get('#step-2-panel') cy.get('#step-2-panel')
.find('dd') .find('dd')
.eq(4) .eq(3)
.should('have.text', '3.00000 (3.03%)'); .should('have.text', '3.00000 (3.03%)');
} }
}); });

View File

@ -4,6 +4,7 @@ import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit'; import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import * as constants from './constants'; import * as constants from './constants';
import { TrafficLight } from '../traffic-light';
interface DealTicketEstimatesProps { interface DealTicketEstimatesProps {
quoteName?: string; quoteName?: string;
@ -13,6 +14,7 @@ interface DealTicketEstimatesProps {
fees?: string; fees?: string;
notionalSize?: string; notionalSize?: string;
size?: string; size?: string;
slippage?: string;
} }
interface DataTitleProps { interface DataTitleProps {
@ -20,7 +22,7 @@ interface DataTitleProps {
quoteName?: string; quoteName?: string;
} }
const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => ( export const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
<dt> <dt>
{children} {children}
{quoteName && <small> ({quoteName})</small>} {quoteName && <small> ({quoteName})</small>}
@ -28,14 +30,20 @@ const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
); );
interface ValueTooltipProps { interface ValueTooltipProps {
value: string; value?: string;
children?: ReactNode;
description: string; description: string;
id?: string; id?: string;
} }
const ValueTooltipRow = ({ value, description, id }: ValueTooltipProps) => ( export const ValueTooltipRow = ({
value,
children,
description,
id,
}: ValueTooltipProps) => (
<dd className="flex gap-x-2 items-center"> <dd className="flex gap-x-2 items-center">
{value} {value || children}
<Tooltip align="center" description={description}> <Tooltip align="center" description={description}>
<div className="cursor-help" id={id || ''} tabIndex={-1}> <div className="cursor-help" id={id || ''} tabIndex={-1}>
<Icon <Icon
@ -56,6 +64,7 @@ export const DealTicketEstimates = ({
fees, fees,
notionalSize, notionalSize,
size, size,
slippage,
}: DealTicketEstimatesProps) => ( }: DealTicketEstimatesProps) => (
<dl className="text-black dark:text-white"> <dl className="text-black dark:text-white">
{size && ( {size && (
@ -93,7 +102,7 @@ export const DealTicketEstimates = ({
</div> </div>
)} )}
{estMargin && ( {estMargin && (
<div className="flex justify-between mb-8"> <div className="flex justify-between mb-2">
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle> <DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
<ValueTooltipRow <ValueTooltipRow
value={estMargin} value={estMargin}
@ -102,17 +111,23 @@ export const DealTicketEstimates = ({
</div> </div>
)} )}
{estCloseOut && ( {estCloseOut && (
<div className="flex justify-between"> <div className="flex justify-between mb-2">
<dt> <DataTitle quoteName={quoteName}>{t('Est. Close out')}</DataTitle>
<span>{t('Est. Close out')}</span>
&nbsp;
<small>({quoteName})</small>
</dt>
<ValueTooltipRow <ValueTooltipRow
value={estCloseOut} value={estCloseOut}
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT} description={constants.EST_CLOSEOUT_TOOLTIP_TEXT}
/> />
</div> </div>
)} )}
{slippage && (
<div className="flex justify-between mb-2">
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
<ValueTooltipRow description={constants.EST_SLIPPAGE}>
<TrafficLight value={parseFloat(slippage)} q1={1} q2={5}>
{slippage}%
</TrafficLight>
</ValueTooltipRow>
</div>
)}
</dl> </dl>
); );

View File

@ -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<HTMLInputElement>) => {
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 (
<div>
<div className="flex justify-between text-black dark:text-white mb-2">
<span data-testid="min-label">{min}</span>
<span data-testid="max-label">{max}</span>
</div>
<SliderRoot
className="mb-2"
value={[value]}
onValueChange={onSliderValueChange}
step={step}
min={min}
max={max}
>
<SliderTrack className="bg-lightGrey dark:bg-offBlack">
<SliderRange className="!bg-black dark:!bg-white" />
</SliderTrack>
<SliderThumb />
</SliderRoot>
<div
data-testid="percentage-selector"
className="flex w-full justify-between text-black dark:text-white mb-6"
>
{sizeRatios.map((size, index) => {
const proportionalSize = size ? (size / 100) * max : min;
return (
<button
className="no-underline hover:underline text-blue"
onClick={() => onButtonValueChange(proportionalSize)}
type="button"
key={index}
>
{getSizeLabel(size)}
</button>
);
})}
</div>
<dl className="text-black dark:text-white">
<div className="flex items-center justify-between">
<dt>{t('Contracts')}</dt>
<dd className="flex justify-end w-full">
<FormGroup
hideLabel={true}
label="Enter Size"
labelFor="trade-size-input"
className="mb-1"
>
<InputSetter
id="input-order-size-market"
type="number"
step={step}
min={min}
max={max}
className="w-full"
value={inputValue}
onChange={onInputValueChange}
>
{inputValue}
</InputSetter>
</FormGroup>
</dd>
</div>
</dl>
</div>
);
};

View File

@ -1,27 +1,13 @@
import React, { useCallback, useState } from 'react'; import React 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 { DealTicketEstimates } from './deal-ticket-estimates'; import { DealTicketEstimates } from './deal-ticket-estimates';
import { InputSetter } from '../input-setter'; import { DealTicketSizeInput } from './deal-ticket-size-input';
import * as constants from './constants';
interface DealTicketSizeProps { interface DealTicketSizeProps {
step: number; step: number;
min: number; min: number;
max: number; max: number;
value: number; size: number;
onValueChange: (value: number[]) => void; onSizeChange: (value: number) => void;
name: string; name: string;
quoteName: string; quoteName: string;
price: string; price: string;
@ -30,173 +16,33 @@ interface DealTicketSizeProps {
fees: string; fees: string;
positionDecimalPlaces: number; positionDecimalPlaces: number;
notionalSize: string; 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 = ({ export const DealTicketSize = ({
value,
step, step,
min, min,
max, max,
price, price,
quoteName, quoteName,
onValueChange, size,
onSizeChange,
estCloseOut, estCloseOut,
positionDecimalPlaces, positionDecimalPlaces,
fees, fees,
notionalSize, notionalSize,
slippage,
}: DealTicketSizeProps) => { }: DealTicketSizeProps) => {
const sizeRatios = [0, 25, 50, 75, 100];
const [inputValue, setInputValue] = useState(value);
const onInputValueChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
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 ? ( return max === 0 ? (
<p>Not enough balance to trade</p> <p>Not enough balance to trade</p>
) : ( ) : (
<div> <div>
<div className="flex justify-between text-black dark:text-white mb-2"> <DealTicketSizeInput
<span data-testid="min-label">{min}</span>
<span data-testid="max-label">{max}</span>
</div>
<SliderRoot
className="mb-2"
value={[value]}
onValueChange={onSliderValueChange}
step={step} step={step}
min={min} min={min}
max={max} max={max}
> value={size}
<SliderTrack className="bg-lightGrey dark:bg-offBlack"> onValueChange={onSizeChange}
<SliderRange className="!bg-black dark:!bg-white" /> positionDecimalPlaces={positionDecimalPlaces}
</SliderTrack> />
<SliderThumb />
</SliderRoot>
<div
data-testid="percentage-selector"
className="flex w-full justify-between text-black dark:text-white mb-6"
>
{sizeRatios.map((size, index) => {
const proportionalSize = size ? (size / 100) * max : min;
return (
<button
className="no-underline hover:underline text-blue"
onClick={() => onButtonValueChange(proportionalSize)}
type="button"
key={index}
>
{getSizeLabel(size)}
</button>
);
})}
</div>
<dl className="text-black dark:text-white">
<div className="flex items-center justify-between mb-4">
<dt>{t('Contracts')}</dt>
<dd className="flex justify-end w-full">
<FormGroup
hideLabel={true}
label="Enter Size"
labelFor="trade-size-input"
>
<InputSetter
id="input-order-size-market"
type="number"
step={step}
min={min}
max={max}
className="w-full"
value={inputValue}
onChange={onInputValueChange}
/>
</FormGroup>
</dd>
</div>
</dl>
{slippage && (
<dl className="text-black dark:text-white">
<div className="flex items-center justify-between mb-8">
<dt>{t('Est. Price Impact / Slippage')}</dt>
<dd
className="flex justify-end gap-x-5"
data-testid="price-slippage-value"
aria-label={t('Est. Price Impact / Slippage')}
>
<span
className={classNames({
'text-darkerGreen dark:text-lightGreen':
parseFloat(slippage) < 1,
'text-amber':
parseFloat(slippage) >= 1 && parseFloat(slippage) < 5,
'text-vega-red': parseFloat(slippage) >= 5,
})}
>
{slippage}%
</span>
<Tooltip align="center" description={constants.EST_SLIPPAGE}>
<div className="cursor-help" tabIndex={-1}>
<Icon
name={IconNames.ISSUE}
className="block rotate-180"
ariaLabel={constants.EST_SLIPPAGE}
/>
</div>
</Tooltip>
</dd>
</div>
</dl>
)}
<DealTicketEstimates <DealTicketEstimates
quoteName={quoteName} quoteName={quoteName}
fees={fees} fees={fees}

View File

@ -0,0 +1,104 @@
import React, { useCallback, useState } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import * as constants from './constants';
import { TrafficLight } from '../traffic-light';
import { Dialog, Icon, Intent, Tooltip } from '@vegaprotocol/ui-toolkit';
import { InputSetter } from '../../components/input-setter';
import { IconNames } from '@blueprintjs/icons';
import { DataTitle, ValueTooltipRow } from './deal-ticket-estimates';
interface DealTicketSlippageProps {
step?: number;
min?: number;
max?: number;
value: number;
onValueChange(value: number): void;
}
export const DealTicketSlippage = ({
value,
step = 0.01,
min = 0,
max = 50,
onValueChange,
}: DealTicketSlippageProps) => {
const [isDialogVisible, setIsDialogVisible] = useState(false);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const numericValue = parseFloat(value);
onValueChange(numericValue);
},
[onValueChange]
);
const toggleDialog = useCallback(() => {
setIsDialogVisible(!isDialogVisible);
}, [isDialogVisible]);
const formLabel = (
<label className="flex items-center mb-1">
<span className="mr-1">{t('Adjust slippage tolerance')}</span>
<Tooltip align="center" description={constants.EST_SLIPPAGE}>
<div className="cursor-help" tabIndex={-1}>
<Icon
name={IconNames.ISSUE}
className="block rotate-180"
ariaLabel={constants.EST_SLIPPAGE}
/>
</div>
</Tooltip>
</label>
);
return (
<>
<Dialog
open={isDialogVisible}
onChange={toggleDialog}
intent={Intent.None}
title={t('Transaction Settings')}
>
<div data-testid="slippage-dialog">
{formLabel}
<InputSetter
id="input-order-slippage"
isInputVisible
hasError={!value}
type="number"
step={step}
min={min}
max={max}
className="w-full"
value={value}
onChange={onChange}
>
{value}%
</InputSetter>
</div>
</Dialog>
<dl className="text-black dark:text-white">
<div className="flex justify-between mb-2">
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
<div className="flex">
<div className="mr-1">
<ValueTooltipRow description={constants.EST_SLIPPAGE}>
<TrafficLight value={value} q1={1} q2={5}>
{value}%
</TrafficLight>
</ValueTooltipRow>
</div>
<button type="button" onClick={toggleDialog}>
<Icon
name={IconNames.COG}
className="block rotate-180"
ariaLabel={t('Override slippage value')}
/>
</button>
</div>
</div>
</dl>
</>
);
};

View File

@ -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 { 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';
@ -10,7 +10,7 @@ import type { Order } from '@vegaprotocol/orders';
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet'; import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
import { import {
t, t,
addDecimal, addDecimalsFormatNumber,
toDecimal, toDecimal,
removeDecimal, removeDecimal,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
@ -33,6 +33,8 @@ import useOrderCloseOut from '../../hooks/use-order-closeout';
import useOrderMargin from '../../hooks/use-order-margin'; import useOrderMargin from '../../hooks/use-order-margin';
import useMaximumPositionSize from '../../hooks/use-maximum-position-size'; import useMaximumPositionSize from '../../hooks/use-maximum-position-size';
import useCalculateSlippage from '../../hooks/use-calculate-slippage'; import useCalculateSlippage from '../../hooks/use-calculate-slippage';
import { Side, OrderType } from '@vegaprotocol/types';
import { DealTicketSlippage } from './deal-ticket-slippage';
interface DealTicketMarketProps { interface DealTicketMarketProps {
market: DealTicketQuery_market; market: DealTicketQuery_market;
@ -62,28 +64,26 @@ export const DealTicketSteps = ({
defaultValues: getDefaultOrder(market), defaultValues: getDefaultOrder(market),
}); });
const [max, setMax] = useState<number | null>(null); const emptyString = ' - ';
const step = toDecimal(market.positionDecimalPlaces); const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type'); const orderType = watch('type');
const orderTimeInForce = watch('timeInForce'); const orderTimeInForce = watch('timeInForce');
const orderSide = watch('side'); const orderSide = watch('side');
const orderSize = watch('size'); const orderSize = watch('size');
const order = watch(); 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 { keypair } = useVegaWallet();
const estMargin = useOrderMargin({ const estMargin = useOrderMargin({
order, order,
market, market,
partyId: keypair?.pub || '', 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<string | null>(null);
const [fees, setFees] = useState<string | null>(null);
const maxTrade = useMaximumPositionSize({ const maxTrade = useMaximumPositionSize({
partyId: keypair?.pub || '', partyId: keypair?.pub || '',
@ -94,45 +94,48 @@ export const DealTicketSteps = ({
price: market?.depth?.lastTrade?.price, price: market?.depth?.lastTrade?.price,
order, order,
}); });
const estCloseOut = useOrderCloseOut({ order, market, partyData });
const slippage = useCalculateSlippage({ marketId: market.id, order }); const slippage = useCalculateSlippage({ marketId: market.id, order });
useEffect(() => { const [slippageValue, setSlippageValue] = useState(
setMax( slippage ? parseFloat(slippage) : 0
new BigNumber(maxTrade) );
.decimalPlaces(market.positionDecimalPlaces) const transactionStatus =
.toNumber() transaction.status === VegaTxStatus.Requested ||
); transaction.status === VegaTxStatus.Pending
}, [maxTrade, market.positionDecimalPlaces]); ? 'pending'
: 'default';
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);
}
};
useEffect(() => { useEffect(() => {
if (market?.depth?.lastTrade?.price) { setSlippageValue(slippage ? parseFloat(slippage) : 0);
const size = new BigNumber(market.depth.lastTrade.price) }, [slippage]);
.multipliedBy(value)
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(); .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) { if (estMargin?.fees && notionalSize) {
const percentage = new BigNumber(estMargin?.fees) const percentage = new BigNumber(estMargin?.fees)
.dividedBy(notionalSize) .dividedBy(notionalSize)
@ -140,17 +143,66 @@ export const DealTicketSteps = ({
.decimalPlaces(2) .decimalPlaces(2)
.toString(); .toString();
setFees(`${estMargin.fees} (${percentage}%)`); return `${estMargin.fees} (${percentage}%)`;
} }
}, [estMargin, notionalSize]);
const transactionStatus = return null;
transaction.status === VegaTxStatus.Requested || }, [estMargin?.fees, notionalSize]);
transaction.status === VegaTxStatus.Pending
? 'pending'
: 'default';
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) => { (order: Order) => {
if (transactionStatus !== 'pending') { if (transactionStatus !== 'pending') {
submit({ submit({
@ -198,25 +250,30 @@ export const DealTicketSteps = ({
label: t('Choose Position Size'), label: t('Choose Position Size'),
component: component:
max !== null ? ( max !== null ? (
<DealTicketSize <>
step={step} <DealTicketSize
min={step} step={step}
max={max} min={step}
onValueChange={onSizeChange} max={max}
value={new BigNumber(orderSize).toNumber()} onSizeChange={onSizeChange}
name="size" size={new BigNumber(orderSize).toNumber()}
price={price || emptyString} name="size"
positionDecimalPlaces={market.positionDecimalPlaces} price={formattedPrice || emptyString}
quoteName={ positionDecimalPlaces={market.positionDecimalPlaces}
market.tradableInstrument.instrument.product.settlementAsset quoteName={
.symbol market.tradableInstrument.instrument.product.settlementAsset
} .symbol
notionalSize={notionalSize || emptyString} }
estCloseOut={estCloseOut} notionalSize={notionalSize || emptyString}
fees={fees || emptyString} estCloseOut={estCloseOut}
estMargin={estMargin?.margin || emptyString} fees={fees || emptyString}
slippage={slippage} estMargin={estMargin?.margin || emptyString}
/> />
<DealTicketSlippage
value={slippageValue}
onValueChange={onSlippageChange}
/>
</>
) : ( ) : (
'loading...' 'loading...'
), ),
@ -240,13 +297,14 @@ export const DealTicketSteps = ({
order={order} order={order}
estCloseOut={estCloseOut} estCloseOut={estCloseOut}
estMargin={estMargin?.margin || emptyString} estMargin={estMargin?.margin || emptyString}
price={price || emptyString} price={formattedPrice || emptyString}
quoteName={ quoteName={
market.tradableInstrument.instrument.product.settlementAsset market.tradableInstrument.instrument.product.settlementAsset
.symbol .symbol
} }
notionalSize={notionalSize || emptyString} notionalSize={notionalSize || emptyString}
fees={fees || emptyString} fees={fees || emptyString}
slippage={slippageValue}
/> />
<Dialog <Dialog
title={getOrderDialogTitle(finalizedOrder?.status)} title={getOrderDialogTitle(finalizedOrder?.status)}

View File

@ -43,6 +43,7 @@ interface Props {
price: string; price: string;
fees: string; fees: string;
notionalSize: string; notionalSize: string;
slippage: number;
} }
export default ({ export default ({
@ -55,6 +56,7 @@ export default ({
fees, fees,
price, price,
notionalSize, notionalSize,
slippage,
}: Props) => { }: Props) => {
const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>( const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>(
MARKET_TAGS_QUERY, MARKET_TAGS_QUERY,
@ -105,6 +107,7 @@ export default ({
fees={fees} fees={fees}
estCloseOut={estCloseOut} estCloseOut={estCloseOut}
notionalSize={notionalSize} notionalSize={notionalSize}
slippage={slippage.toString()}
/> />
<div className="mt-12 max-w-sm"> <div className="mt-12 max-w-sm">

View File

@ -1,20 +1,19 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import { Input } from '@vegaprotocol/ui-toolkit'; import { Input } from '@vegaprotocol/ui-toolkit';
import type { InputProps } from '@vegaprotocol/ui-toolkit'; import type { InputProps } from '@vegaprotocol/ui-toolkit';
interface InputSetterProps { interface InputSetterProps {
buttonLabel?: string; buttonLabel?: string;
value: string | number;
isInputVisible?: boolean; isInputVisible?: boolean;
onValueChange?: () => string; children?: ReactNode;
} }
export const InputSetter = ({ export const InputSetter = ({
buttonLabel = t('set'), buttonLabel = t('set'),
value = '',
isInputVisible = false, isInputVisible = false,
onValueChange, children,
...props ...props
}: InputSetterProps & InputProps) => { }: InputSetterProps & InputProps) => {
const [isInputToggled, setIsInputToggled] = useState(isInputVisible); const [isInputToggled, setIsInputToggled] = useState(isInputVisible);
@ -35,7 +34,7 @@ export const InputSetter = ({
return isInputToggled ? ( return isInputToggled ? (
<div className="flex items-center"> <div className="flex items-center">
<Input {...props} value={value} onKeyDown={onInputEnter} /> <Input {...props} onKeyDown={onInputEnter} />
<button <button
type="button" type="button"
className="no-underline hover:underline text-blue ml-2" className="no-underline hover:underline text-blue ml-2"
@ -47,10 +46,10 @@ export const InputSetter = ({
) : ( ) : (
<button <button
type="button" type="button"
className="no-underline hover:underline text-blue" className="no-underline hover:underline text-blue py-1.5"
onClick={toggleInput} onClick={toggleInput}
> >
{value} {children || props.value}
</button> </button>
); );
}; };

View File

@ -88,7 +88,7 @@ export const Stepper = ({ steps }: StepperProps) => {
'md:mt-0 font-alpha uppercase text-black dark:text-white', 'md:mt-0 font-alpha uppercase text-black dark:text-white',
{ {
'mt-2 text-md md:text-2xl': isActive, 'mt-2 text-md md:text-2xl': isActive,
'mt-4 text-sm md:text-lg md:ml-8': !isActive, 'mt-4 text-sm md:text-lg md:ml-2': !isActive,
} }
)} )}
> >

View File

@ -0,0 +1 @@
export * from './traffic-light';

View File

@ -0,0 +1,25 @@
import React from 'react';
import type { ReactNode } from 'react';
import classNames from 'classnames';
interface TrafficLightProps {
value: number;
q1: number;
q2: number;
children: ReactNode;
}
export const TrafficLight = ({
value,
q1,
q2,
children,
}: TrafficLightProps) => {
const slippageClassName = classNames({
'text-darkerGreen dark:text-lightGreen': value < q1,
'text-amber': value >= q1 && value < q2,
'text-vega-red': value >= q2,
});
return <div className={slippageClassName}>{children || value}</div>;
};