feat(deal-ticket): submit oco stop orders (#4539)
This commit is contained in:
parent
4684745382
commit
6a9f15f59e
@ -64,7 +64,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-error-message-price-limit').should(
|
||||
cy.getByTestId('deal-ticket-error-message-price').should(
|
||||
'have.text',
|
||||
'Price accepts up to 5 decimal places'
|
||||
);
|
||||
@ -87,7 +87,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(orderSizeField).clear().type('1.234');
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('deal-ticket-error-message-size-market').should(
|
||||
cy.getByTestId('deal-ticket-error-message-size').should(
|
||||
'have.text',
|
||||
'Size must be whole numbers for this market'
|
||||
);
|
||||
@ -96,7 +96,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
it('must warn if order size is set to 0', function () {
|
||||
cy.getByTestId(orderSizeField).clear().type('0');
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('deal-ticket-error-message-size-market').should(
|
||||
cy.getByTestId('deal-ticket-error-message-size').should(
|
||||
'have.text',
|
||||
'Size cannot be lower than 1'
|
||||
);
|
||||
|
@ -36,6 +36,6 @@ export const StopOrdersContainer = () => {
|
||||
|
||||
const useStopOrdersStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_fills_store',
|
||||
name: 'vega_stop_orders_store',
|
||||
})
|
||||
);
|
||||
|
@ -209,3 +209,15 @@ html [data-theme='dark'] {
|
||||
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
import type { Control } from 'react-hook-form';
|
||||
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
|
||||
export interface DealTicketAmountProps {
|
||||
control: Control<OrderFormValues>;
|
||||
type: Schema.OrderType;
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string;
|
||||
market: Market;
|
||||
sizeError?: string;
|
||||
priceError?: string;
|
||||
}
|
||||
|
||||
export const DealTicketAmount = ({
|
||||
type,
|
||||
marketData,
|
||||
marketPrice,
|
||||
...props
|
||||
}: DealTicketAmountProps) => {
|
||||
switch (type) {
|
||||
case Schema.OrderType.TYPE_MARKET:
|
||||
return (
|
||||
<DealTicketMarketAmount
|
||||
{...props}
|
||||
marketData={marketData}
|
||||
marketPrice={marketPrice}
|
||||
/>
|
||||
);
|
||||
case Schema.OrderType.TYPE_LIMIT:
|
||||
return <DealTicketLimitAmount {...props} />;
|
||||
default: {
|
||||
throw new Error('Invalid ticket type ' + type);
|
||||
}
|
||||
}
|
||||
};
|
@ -1,120 +0,0 @@
|
||||
import {
|
||||
TradingFormGroup,
|
||||
TradingInput,
|
||||
TradingInputError,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
export type DealTicketLimitAmountProps = Omit<
|
||||
DealTicketAmountProps,
|
||||
'marketData' | 'type'
|
||||
>;
|
||||
|
||||
export const DealTicketLimitAmount = ({
|
||||
control,
|
||||
market,
|
||||
sizeError,
|
||||
priceError,
|
||||
}: DealTicketLimitAmountProps) => {
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
|
||||
const renderError = () => {
|
||||
if (sizeError) {
|
||||
return (
|
||||
<TradingInputError testId="deal-ticket-error-message-size-limit">
|
||||
{sizeError}
|
||||
</TradingInputError>
|
||||
);
|
||||
}
|
||||
|
||||
if (priceError) {
|
||||
return (
|
||||
<TradingInputError testId="deal-ticket-error-message-price-limit">
|
||||
{priceError}
|
||||
</TradingInputError>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<TradingFormGroup
|
||||
label={t('Size')}
|
||||
labelFor="input-order-size-limit"
|
||||
className="!mb-0"
|
||||
>
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<TradingInput
|
||||
id="input-order-size-limit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
hasError={!!fieldState.error}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
<div className="pt-5 leading-10">@</div>
|
||||
<div className="flex-1">
|
||||
<TradingFormGroup
|
||||
labelFor="input-price-quote"
|
||||
label={t(`Price (${quoteName})`)}
|
||||
labelAlign="right"
|
||||
className="!mb-0"
|
||||
>
|
||||
<Controller
|
||||
name="price"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<TradingInput
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
hasError={!!fieldState.error}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
{renderError()}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,102 +0,0 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
toDecimal,
|
||||
validateAmount,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
TradingInput,
|
||||
TradingInputError,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { isMarketInAuction } from '@vegaprotocol/markets';
|
||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
|
||||
|
||||
export const DealTicketMarketAmount = ({
|
||||
control,
|
||||
market,
|
||||
marketData,
|
||||
marketPrice,
|
||||
sizeError,
|
||||
}: DealTicketMarketAmountProps) => {
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
const price = marketPrice;
|
||||
|
||||
const priceFormatted = price
|
||||
? addDecimalsFormatNumber(price, market.decimalPlaces)
|
||||
: undefined;
|
||||
|
||||
const inAuction = isMarketInAuction(marketData.marketTradingMode);
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 text-xs">{t('Size')}</div>
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<TradingInput
|
||||
id="input-order-size-market"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
hasError={!!fieldState.error}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-5 leading-10">@</div>
|
||||
<div className="flex-1 text-sm text-right">
|
||||
{inAuction && (
|
||||
<Tooltip
|
||||
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.'
|
||||
)}
|
||||
>
|
||||
<div className="mb-2">{t(`Indicative price`)}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div
|
||||
data-testid="last-price"
|
||||
className={classNames('leading-10', { 'pt-5': !inAuction })}
|
||||
>
|
||||
{priceFormatted && quoteName ? (
|
||||
<>
|
||||
~{priceFormatted} {quoteName}
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{sizeError && (
|
||||
<TradingInputError
|
||||
intent="danger"
|
||||
testId="deal-ticket-error-message-size-market"
|
||||
>
|
||||
{sizeError}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -72,6 +72,7 @@ const timeInForce = 'order-tif';
|
||||
const sizeErrorMessage = 'stop-order-error-message-size';
|
||||
const priceErrorMessage = 'stop-order-error-message-price';
|
||||
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
|
||||
const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
|
||||
const triggerTrailingPercentOffsetErrorMessage =
|
||||
'stop-order-error-message-trigger-trailing-percent-offset';
|
||||
|
||||
@ -114,14 +115,6 @@ describe('StopOrder', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should display trigger price as price for market type order', async () => {
|
||||
render(generateJsx());
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '10');
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('10.0');
|
||||
});
|
||||
|
||||
it('should use local storage state for initial values', async () => {
|
||||
const values: Partial<StopOrderFormValues> = {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
@ -203,8 +196,8 @@ describe('StopOrder', () => {
|
||||
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
// price error message should not show if size has error
|
||||
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||
// expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||
// await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
@ -249,10 +242,17 @@ describe('StopOrder', () => {
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
|
||||
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// clear and fill using valid value
|
||||
// clear and fill using value causing immediate trigger
|
||||
await userEvent.clear(screen.getByTestId(triggerPriceInput));
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
|
||||
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||
expect(
|
||||
screen.queryByTestId(triggerPriceWarningMessage)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// change to correct value
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '2');
|
||||
expect(screen.queryByTestId(triggerPriceWarningMessage)).toBeNull();
|
||||
});
|
||||
|
||||
it('validates trigger trailing percentage offset field', async () => {
|
||||
|
@ -1,22 +1,18 @@
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
formatNumber,
|
||||
removeDecimal,
|
||||
toDecimal,
|
||||
validateAmount,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { removeDecimal, toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||
import type { Control, UseFormWatch } from 'react-hook-form';
|
||||
import { useForm, Controller, useController } from 'react-hook-form';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
TradingRadio,
|
||||
TradingRadioGroup,
|
||||
TradingInput,
|
||||
TradingCheckbox,
|
||||
TradingFormGroup,
|
||||
TradingInputError,
|
||||
TradingSelect,
|
||||
TradingRadio as Radio,
|
||||
TradingRadioGroup as RadioGroup,
|
||||
TradingInput as Input,
|
||||
TradingCheckbox as Checkbox,
|
||||
TradingFormGroup as FormGroup,
|
||||
TradingInputError as InputError,
|
||||
TradingSelect as Select,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
||||
@ -49,6 +45,8 @@ export interface StopOrderProps {
|
||||
submit: (order: StopOrdersSubmission) => void;
|
||||
}
|
||||
|
||||
const trailingPercentOffsetStep = '0.1';
|
||||
|
||||
const getDefaultValues = (
|
||||
type: Schema.OrderType,
|
||||
storedValues?: Partial<StopOrderFormValues>
|
||||
@ -62,9 +60,425 @@ const getDefaultValues = (
|
||||
expire: false,
|
||||
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||
size: '0',
|
||||
ocoType: type,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
ocoTriggerType: 'price',
|
||||
ocoSize: '0',
|
||||
...storedValues,
|
||||
});
|
||||
|
||||
const Trigger = ({
|
||||
control,
|
||||
watch,
|
||||
priceStep,
|
||||
assetSymbol,
|
||||
oco,
|
||||
marketPrice,
|
||||
decimalPlaces,
|
||||
}: {
|
||||
control: Control<StopOrderFormValues>;
|
||||
watch: UseFormWatch<StopOrderFormValues>;
|
||||
priceStep: string;
|
||||
assetSymbol: string;
|
||||
oco?: boolean;
|
||||
marketPrice?: string | null;
|
||||
decimalPlaces: number;
|
||||
}) => {
|
||||
const triggerType = watch(oco ? 'ocoTriggerType' : 'triggerType');
|
||||
const triggerDirection = watch('triggerDirection');
|
||||
const isPriceTrigger = triggerType === 'price';
|
||||
return (
|
||||
<FormGroup label={t('Trigger')} labelFor="">
|
||||
<Controller
|
||||
name="triggerDirection"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { value, onChange } = field;
|
||||
return (
|
||||
<RadioGroup
|
||||
name="triggerDirection"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
className="mb-2"
|
||||
>
|
||||
<Radio
|
||||
value={
|
||||
oco
|
||||
? Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
: Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
}
|
||||
id={`triggerDirection-risesAbove${oco ? '-oco' : ''}`}
|
||||
label={'Rises above'}
|
||||
/>
|
||||
<Radio
|
||||
value={
|
||||
!oco
|
||||
? Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
: Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
}
|
||||
id={`triggerDirection-fallsBelow${oco ? '-oco' : ''}`}
|
||||
label={'Falls below'}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{isPriceTrigger && (
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name={oco ? 'ocoTriggerPrice' : 'triggerPrice'}
|
||||
rules={{
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
let triggerWarning = false;
|
||||
|
||||
if (marketPrice && value) {
|
||||
const condition =
|
||||
(!oco &&
|
||||
triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_RISES_ABOVE) ||
|
||||
(oco &&
|
||||
triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_FALLS_BELOW)
|
||||
? '>'
|
||||
: '<';
|
||||
const diff =
|
||||
BigInt(marketPrice) -
|
||||
BigInt(removeDecimal(value, decimalPlaces));
|
||||
if (
|
||||
(condition === '>' && diff > 0) ||
|
||||
(condition === '<' && diff < 0)
|
||||
) {
|
||||
triggerWarning = true;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
data-testid={`triggerPrice${oco ? '-oco' : ''}`}
|
||||
type="number"
|
||||
step={priceStep}
|
||||
appendElement={assetSymbol}
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{fieldState.error && (
|
||||
<InputError
|
||||
testId={`stop-order-error-message-trigger-price${
|
||||
oco ? '-oco' : ''
|
||||
}`}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
{!fieldState.error && triggerWarning && (
|
||||
<InputError
|
||||
intent="warning"
|
||||
testId={`stop-order-warning-message-trigger-price${
|
||||
oco ? '-oco' : ''
|
||||
}`}
|
||||
>
|
||||
{t('Stop order will be triggered immediately')}
|
||||
</InputError>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isPriceTrigger && (
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name={
|
||||
oco
|
||||
? 'ocoTriggerTrailingPercentOffset'
|
||||
: 'triggerTrailingPercentOffset'
|
||||
}
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a trailing percent offset'),
|
||||
min: {
|
||||
value: trailingPercentOffsetStep,
|
||||
message: t(
|
||||
'Trailing percent offset cannot be lower than ' +
|
||||
trailingPercentOffsetStep
|
||||
),
|
||||
},
|
||||
max: {
|
||||
value: '99.9',
|
||||
message: t(
|
||||
'Trailing percent offset cannot be higher than 99.9'
|
||||
),
|
||||
},
|
||||
validate: validateAmount(
|
||||
trailingPercentOffsetStep,
|
||||
'Trailing percentage offset'
|
||||
),
|
||||
}}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
type="number"
|
||||
step={trailingPercentOffsetStep}
|
||||
appendElement="%"
|
||||
data-testid={`triggerTrailingPercentOffset${
|
||||
oco ? '-oco' : ''
|
||||
}`}
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{fieldState.error && (
|
||||
<InputError
|
||||
testId={`stop-order-error-message-trigger-trailing-percent-offset${
|
||||
oco ? '-oco' : ''
|
||||
}`}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
name={oco ? 'ocoTriggerType' : 'triggerType'}
|
||||
control={control}
|
||||
rules={{
|
||||
deps: oco
|
||||
? ['ocoTriggerTrailingPercentOffset', 'ocoTriggerPrice']
|
||||
: ['triggerTrailingPercentOffset', 'triggerPrice'],
|
||||
}}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<RadioGroup
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<Radio
|
||||
value="price"
|
||||
id={`triggerType-price${oco ? '-oco' : ''}`}
|
||||
label={'Price'}
|
||||
/>
|
||||
<Radio
|
||||
value="trailingPercentOffset"
|
||||
id={`triggerType-trailingPercentOffset${oco ? '-oco' : ''}`}
|
||||
label={'Trailing Percent Offset'}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const Size = ({
|
||||
control,
|
||||
sizeStep,
|
||||
oco,
|
||||
}: {
|
||||
control: Control<StopOrderFormValues>;
|
||||
sizeStep: string;
|
||||
oco?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Controller
|
||||
name={oco ? 'ocoSize' : 'size'}
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
const id = `order-size${oco ? '-oco' : ''}`;
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<FormGroup labelFor={id} label={t(`Size`)} compact>
|
||||
<Input
|
||||
id={id}
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid={id}
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError
|
||||
testId={`stop-order-error-message-size${oco ? '-oco' : ''}`}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Price = ({
|
||||
control,
|
||||
watch,
|
||||
priceStep,
|
||||
quoteName,
|
||||
oco,
|
||||
}: {
|
||||
control: Control<StopOrderFormValues>;
|
||||
watch: UseFormWatch<StopOrderFormValues>;
|
||||
priceStep: string;
|
||||
quoteName: string;
|
||||
oco?: boolean;
|
||||
}) => {
|
||||
if (watch(oco ? 'ocoType' : 'type') === Schema.OrderType.TYPE_MARKET) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Controller
|
||||
name={oco ? 'ocoPrice' : 'price'}
|
||||
control={control}
|
||||
rules={{
|
||||
deps: 'type',
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
const id = `order-price${oco ? '-oco' : ''}`;
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<FormGroup
|
||||
labelFor={id}
|
||||
label={t(`Price (${quoteName})`)}
|
||||
compact={true}
|
||||
>
|
||||
<Input
|
||||
id={id}
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid={id}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError
|
||||
testId={`stop-order-error-message-price${oco ? '-oco' : ''}`}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TimeInForce = ({
|
||||
control,
|
||||
oco,
|
||||
}: {
|
||||
control: Control<StopOrderFormValues>;
|
||||
oco?: boolean;
|
||||
}) => (
|
||||
<Controller
|
||||
name="timeInForce"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => {
|
||||
const id = `select-time-in-force${oco ? '-oco' : ''}`;
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<FormGroup label={t('Time in force')} labelFor={id} compact={true}>
|
||||
<Select
|
||||
id={id}
|
||||
className="w-full"
|
||||
data-testid="order-tif"
|
||||
hasError={!!fieldState.error}
|
||||
{...field}
|
||||
>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
|
||||
</option>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
|
||||
</option>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId={`stop-error-message-tif${oco ? '-oco' : ''}`}>
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const ReduceOnly = () => (
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
label={
|
||||
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
|
||||
<>{t('Reduce only')}</>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setType = useDealTicketFormValues((state) => state.setType);
|
||||
@ -107,6 +521,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
const timeInForce = watch('timeInForce');
|
||||
const rawPrice = watch('price');
|
||||
const rawSize = watch('size');
|
||||
const oco = watch('oco');
|
||||
|
||||
useEffect(() => {
|
||||
const size = storedFormValues?.[dealTicketType]?.size;
|
||||
@ -155,12 +570,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const trailingPercentOffsetStep = '0.1';
|
||||
|
||||
const priceFormatted =
|
||||
isPriceTrigger && triggerPrice
|
||||
? formatNumber(triggerPrice, market.decimalPlaces)
|
||||
: undefined;
|
||||
|
||||
useController({
|
||||
name: 'type',
|
||||
@ -187,9 +596,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
}}
|
||||
/>
|
||||
{errors.type && (
|
||||
<TradingInputError testId="stop-order-error-message-type">
|
||||
<InputError testId="stop-order-error-message-type">
|
||||
{errors.type.message}
|
||||
</TradingInputError>
|
||||
</InputError>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
@ -199,302 +608,117 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
<TradingFormGroup label={t('Trigger')} compact={true} labelFor="">
|
||||
<Controller
|
||||
name="triggerDirection"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<TradingRadioGroup
|
||||
name="triggerDirection"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
className="mb-2"
|
||||
>
|
||||
<TradingRadio
|
||||
value={
|
||||
Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
}
|
||||
id="triggerDirection-risesAbove"
|
||||
label={'Rises above'}
|
||||
/>
|
||||
<TradingRadio
|
||||
value={
|
||||
Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
}
|
||||
id="triggerDirection-fallsBelow"
|
||||
label={'Falls below'}
|
||||
/>
|
||||
</TradingRadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{isPriceTrigger && (
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="triggerPrice"
|
||||
rules={{
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<TradingInput
|
||||
data-testid="triggerPrice"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
appendElement={asset.symbol}
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.triggerPrice && (
|
||||
<TradingInputError testId="stop-order-error-message-trigger-price">
|
||||
{errors.triggerPrice.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isPriceTrigger && (
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="triggerTrailingPercentOffset"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a trailing percent offset'),
|
||||
min: {
|
||||
value: trailingPercentOffsetStep,
|
||||
message: t(
|
||||
'Trailing percent offset cannot be lower than ' +
|
||||
trailingPercentOffsetStep
|
||||
),
|
||||
},
|
||||
max: {
|
||||
value: '99.9',
|
||||
message: t(
|
||||
'Trailing percent offset cannot be higher than 99.9'
|
||||
),
|
||||
},
|
||||
validate: validateAmount(
|
||||
trailingPercentOffsetStep,
|
||||
'Trailing percentage offset'
|
||||
),
|
||||
}}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<TradingInput
|
||||
type="number"
|
||||
step={trailingPercentOffsetStep}
|
||||
appendElement="%"
|
||||
data-testid="triggerTrailingPercentOffset"
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.triggerTrailingPercentOffset && (
|
||||
<TradingInputError testId="stop-order-error-message-trigger-trailing-percent-offset">
|
||||
{errors.triggerTrailingPercentOffset.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
name="triggerType"
|
||||
control={control}
|
||||
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<TradingRadioGroup
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<TradingRadio
|
||||
value="price"
|
||||
id="triggerType-price"
|
||||
label={'Price'}
|
||||
/>
|
||||
<TradingRadio
|
||||
value="trailingPercentOffset"
|
||||
id="triggerType-trailingPercentOffset"
|
||||
label={'Trailing Percent Offset'}
|
||||
/>
|
||||
</TradingRadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
<div className="mb-2">
|
||||
<div className="flex items-start gap-4">
|
||||
<TradingFormGroup
|
||||
labelFor="input-price-quote"
|
||||
label={t(`Size`)}
|
||||
className="!mb-0 flex-1"
|
||||
>
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<TradingInput
|
||||
id="order-size"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
<div className="pt-5 leading-10">@</div>
|
||||
<div className="flex-1">
|
||||
{type === Schema.OrderType.TYPE_LIMIT ? (
|
||||
<TradingFormGroup
|
||||
labelFor="input-price-quote"
|
||||
label={t(`Price (${quoteName})`)}
|
||||
labelAlign="right"
|
||||
className="!mb-0"
|
||||
>
|
||||
<Controller
|
||||
name="price"
|
||||
control={control}
|
||||
rules={{
|
||||
deps: 'type',
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
render={({ field, fieldState }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<TradingInput
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
value={value || ''}
|
||||
hasError={!!fieldState.error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
) : (
|
||||
<div
|
||||
className="text-sm text-right pt-5 leading-10"
|
||||
data-testid="price"
|
||||
>
|
||||
{priceFormatted && quoteName
|
||||
? `~${priceFormatted} ${quoteName}`
|
||||
: '-'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{errors.size && (
|
||||
<TradingInputError testId="stop-order-error-message-size">
|
||||
{errors.size.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
|
||||
{!errors.size &&
|
||||
errors.price &&
|
||||
type === Schema.OrderType.TYPE_LIMIT && (
|
||||
<TradingInputError testId="stop-order-error-message-price">
|
||||
{errors.price.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<TradingFormGroup
|
||||
label={t('Time in force')}
|
||||
labelFor="select-time-in-force"
|
||||
compact={true}
|
||||
>
|
||||
<Controller
|
||||
name="timeInForce"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TradingSelect
|
||||
id="select-time-in-force"
|
||||
className="w-full"
|
||||
data-testid="order-tif"
|
||||
hasError={!!fieldState.error}
|
||||
{...field}
|
||||
>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
|
||||
</option>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
|
||||
</option>
|
||||
</TradingSelect>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{errors.timeInForce && (
|
||||
<TradingInputError testId="stop-error-message-tif">
|
||||
{errors.timeInForce.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
<Trigger
|
||||
control={control}
|
||||
watch={watch}
|
||||
priceStep={priceStep}
|
||||
assetSymbol={asset.symbol}
|
||||
marketPrice={marketPrice}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
/>
|
||||
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
|
||||
<Price
|
||||
control={control}
|
||||
watch={watch}
|
||||
priceStep={priceStep}
|
||||
quoteName={quoteName}
|
||||
/>
|
||||
<Size control={control} sizeStep={sizeStep} />
|
||||
<TimeInForce control={control} />
|
||||
<div className="flex gap-2 pb-3 justify-end">
|
||||
<ReduceOnly />
|
||||
</div>
|
||||
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
<Controller
|
||||
name="oco"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<Checkbox
|
||||
onCheckedChange={(state) => {
|
||||
onChange(state);
|
||||
setValue(
|
||||
'expiryStrategy',
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||
);
|
||||
}}
|
||||
checked={value}
|
||||
name="oco"
|
||||
label={
|
||||
<Tooltip
|
||||
description={<span>{t('One cancels another')}</span>}
|
||||
>
|
||||
<>{t('OCO')}</>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{oco && (
|
||||
<>
|
||||
<FormGroup label={t('Type')} labelFor="">
|
||||
<Controller
|
||||
name={`ocoType`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<RadioGroup
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<Radio
|
||||
value={Schema.OrderType.TYPE_MARKET}
|
||||
id={`ocoTypeMarket`}
|
||||
label={'Market'}
|
||||
/>
|
||||
<Radio
|
||||
value={Schema.OrderType.TYPE_LIMIT}
|
||||
id={`ocoTypeLimit`}
|
||||
label={'Limit'}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Trigger
|
||||
control={control}
|
||||
watch={watch}
|
||||
priceStep={priceStep}
|
||||
assetSymbol={asset.symbol}
|
||||
marketPrice={marketPrice}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
oco
|
||||
/>
|
||||
<hr className="mb-2 border-vega-clight-500 dark:border-vega-cdark-500" />
|
||||
<Price
|
||||
control={control}
|
||||
watch={watch}
|
||||
priceStep={priceStep}
|
||||
quoteName={quoteName}
|
||||
oco
|
||||
/>
|
||||
<Size control={control} sizeStep={sizeStep} oco />
|
||||
<TimeInForce control={control} oco />
|
||||
<div className="flex gap-2 mb-2 justify-end">
|
||||
<ReduceOnly />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="expire"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: onCheckedChange, value } = field;
|
||||
return (
|
||||
<TradingCheckbox
|
||||
<Checkbox
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={value}
|
||||
name="expire"
|
||||
@ -503,50 +727,42 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<TradingCheckbox
|
||||
name="reduce-only"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
label={
|
||||
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
|
||||
<>{t('Reduce only')}</>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{expire && (
|
||||
<>
|
||||
<TradingFormGroup
|
||||
label={t('Strategy')}
|
||||
labelFor="expiryStrategy"
|
||||
compact={true}
|
||||
>
|
||||
<FormGroup label={t('Strategy')} labelFor="expiryStrategy">
|
||||
<Controller
|
||||
name="expiryStrategy"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<TradingRadioGroup orientation="horizontal" {...field}>
|
||||
<TradingRadio
|
||||
<RadioGroup
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<Radio
|
||||
disabled={oco}
|
||||
value={
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||
}
|
||||
id="expiryStrategy-submit"
|
||||
label={'Submit'}
|
||||
/>
|
||||
<TradingRadio
|
||||
<Radio
|
||||
value={
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||
}
|
||||
id="expiryStrategy-cancel"
|
||||
label={'Cancel'}
|
||||
/>
|
||||
</TradingRadioGroup>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
<div className="mb-2">
|
||||
</FormGroup>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
name="expiresAt"
|
||||
control={control}
|
||||
|
@ -7,7 +7,6 @@ import { DealTicket } from './deal-ticket';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import type { OrdersQuery } from '@vegaprotocol/orders';
|
||||
import {
|
||||
DealTicketType,
|
||||
@ -135,20 +134,6 @@ describe('DealTicket', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should display last price for market type order', () => {
|
||||
render(generateJsx());
|
||||
act(() => {
|
||||
screen.getByTestId('order-type-Market').click();
|
||||
});
|
||||
// Assert last price is shown
|
||||
expect(screen.getByTestId('last-price')).toHaveTextContent(
|
||||
// eslint-disable-next-line
|
||||
`~${addDecimal(marketPrice, market.decimalPlaces)} ${
|
||||
market.tradableInstrument.instrument.product.quoteName
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should use local storage state for initial values', () => {
|
||||
const expectedOrder = {
|
||||
marketId: market.id,
|
||||
|
@ -3,7 +3,6 @@ import * as Schema from '@vegaprotocol/types';
|
||||
import type { FormEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { Controller, useController, useForm } from 'react-hook-form';
|
||||
import { DealTicketAmount } from './deal-ticket-amount';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import {
|
||||
DealTicketFeeDetails,
|
||||
@ -17,8 +16,10 @@ import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
|
||||
import {
|
||||
TradingCheckbox,
|
||||
TradingInputError,
|
||||
TradingInput as Input,
|
||||
TradingCheckbox as Checkbox,
|
||||
TradingFormGroup as FormGroup,
|
||||
TradingInputError as InputError,
|
||||
Intent,
|
||||
Notification,
|
||||
Tooltip,
|
||||
@ -28,7 +29,12 @@ import {
|
||||
useEstimatePositionQuery,
|
||||
useOpenVolume,
|
||||
} from '@vegaprotocol/positions';
|
||||
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
||||
import {
|
||||
toBigNum,
|
||||
removeDecimal,
|
||||
validateAmount,
|
||||
toDecimal,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||
import type { OrderInfo } from '@vegaprotocol/types';
|
||||
@ -332,6 +338,10 @@ export const DealTicket = ({
|
||||
},
|
||||
});
|
||||
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={
|
||||
@ -366,15 +376,82 @@ export const DealTicket = ({
|
||||
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
<DealTicketAmount
|
||||
type={type}
|
||||
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
marketPrice={marketPrice || undefined}
|
||||
sizeError={errors.size?.message}
|
||||
priceError={errors.price?.message}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-4">
|
||||
<FormGroup
|
||||
label={t('Size')}
|
||||
labelFor="input-order-size-limit"
|
||||
compact
|
||||
>
|
||||
<Input
|
||||
id="input-order-size-limit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId="deal-ticket-error-message-size">
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{type === Schema.OrderType.TYPE_LIMIT && (
|
||||
<Controller
|
||||
name="price"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-4">
|
||||
<FormGroup
|
||||
labelFor="input-price-quote"
|
||||
label={t(`Price (${quoteName})`)}
|
||||
compact
|
||||
>
|
||||
<Input
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId="deal-ticket-error-message-price">
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="timeInForce"
|
||||
control={control}
|
||||
@ -417,7 +494,7 @@ export const DealTicket = ({
|
||||
name="postOnly"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TradingCheckbox
|
||||
<Checkbox
|
||||
name="post-only"
|
||||
checked={!disablePostOnlyCheckbox && field.value}
|
||||
disabled={disablePostOnlyCheckbox}
|
||||
@ -449,7 +526,7 @@ export const DealTicket = ({
|
||||
name="reduceOnly"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TradingCheckbox
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={!disableReduceOnlyCheckbox && field.value}
|
||||
disabled={disableReduceOnlyCheckbox}
|
||||
@ -483,7 +560,7 @@ export const DealTicket = ({
|
||||
name="iceberg"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TradingCheckbox
|
||||
<Checkbox
|
||||
name="iceberg"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
@ -572,11 +649,11 @@ export const NoWalletWarning = ({
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<TradingInputError testId="deal-ticket-error-message-summary">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
{
|
||||
'You need to connect your own wallet to start trading on this market'
|
||||
}
|
||||
</TradingInputError>
|
||||
</InputError>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -613,9 +690,9 @@ const SummaryMessage = memo(
|
||||
if (error?.message) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<TradingInputError testId="deal-ticket-error-message-summary">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
{error?.message}
|
||||
</TradingInputError>
|
||||
</InputError>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -23,25 +23,27 @@ export const ExpirySelector = ({
|
||||
const dateFormatted = formatForInput(date);
|
||||
const minDate = formatForInput(date);
|
||||
return (
|
||||
<TradingFormGroup
|
||||
label={t('Expiry time/date')}
|
||||
labelFor="expiration"
|
||||
compact={true}
|
||||
>
|
||||
<TradingInput
|
||||
data-testid="date-picker-field"
|
||||
id="expiration"
|
||||
type="datetime-local"
|
||||
value={dateFormatted}
|
||||
onChange={(e) => onSelect(e.target.value)}
|
||||
min={minDate}
|
||||
hasError={!!errorMessage}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<TradingInputError testId="deal-ticket-error-message-expiry">
|
||||
{errorMessage}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<div className="mb-4">
|
||||
<TradingFormGroup
|
||||
label={t('Expiry time/date')}
|
||||
labelFor="expiration"
|
||||
compact
|
||||
>
|
||||
<TradingInput
|
||||
data-testid="date-picker-field"
|
||||
id="expiration"
|
||||
type="datetime-local"
|
||||
value={dateFormatted}
|
||||
onChange={(e) => onSelect(e.target.value)}
|
||||
min={minDate}
|
||||
hasError={!!errorMessage}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<TradingInputError testId="deal-ticket-error-message-expiry">
|
||||
{errorMessage}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,4 @@
|
||||
export * from './deal-ticket-amount';
|
||||
export * from './deal-ticket-container';
|
||||
export * from './deal-ticket-limit-amount';
|
||||
export * from './deal-ticket-market-amount';
|
||||
export * from './deal-ticket';
|
||||
export * from './deal-ticket-stop-order';
|
||||
export * from './deal-ticket-container';
|
||||
|
@ -90,32 +90,34 @@ export const TimeInForceSelector = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<TradingFormGroup
|
||||
label={t('Time in force')}
|
||||
labelFor="select-time-in-force"
|
||||
compact={true}
|
||||
>
|
||||
<TradingSelect
|
||||
id="select-time-in-force"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onSelect(e.target.value as Schema.OrderTimeInForce);
|
||||
}}
|
||||
className="w-full"
|
||||
data-testid="order-tif"
|
||||
hasError={!!errorMessage}
|
||||
<div className="mb-4">
|
||||
<TradingFormGroup
|
||||
label={t('Time in force')}
|
||||
labelFor="select-time-in-force"
|
||||
compact={true}
|
||||
>
|
||||
{options.map(([key, value]) => (
|
||||
<option key={key} value={value}>
|
||||
{timeInForceLabel(value)}
|
||||
</option>
|
||||
))}
|
||||
</TradingSelect>
|
||||
{errorMessage && (
|
||||
<TradingInputError testId="deal-ticket-error-message-tif">
|
||||
{renderError(errorMessage)}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingSelect
|
||||
id="select-time-in-force"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onSelect(e.target.value as Schema.OrderTimeInForce);
|
||||
}}
|
||||
className="w-full"
|
||||
data-testid="order-tif"
|
||||
hasError={!!errorMessage}
|
||||
>
|
||||
{options.map(([key, value]) => (
|
||||
<option key={key} value={value}>
|
||||
{timeInForceLabel(value)}
|
||||
</option>
|
||||
))}
|
||||
</TradingSelect>
|
||||
{errorMessage && (
|
||||
<TradingInputError testId="deal-ticket-error-message-tif">
|
||||
{renderError(errorMessage)}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -76,7 +76,7 @@ export const TypeToggle = ({
|
||||
<TradingDropdownTrigger
|
||||
data-testid="order-type-Stop"
|
||||
className={classNames(
|
||||
'rounded px-3 flex flex-nowrap items-center justify-center',
|
||||
'rounded px-2 flex flex-nowrap items-center justify-center',
|
||||
{
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': selectedOption,
|
||||
}
|
||||
|
@ -28,6 +28,17 @@ export interface StopOrderFormValues {
|
||||
expire: boolean;
|
||||
expiryStrategy?: Schema.StopOrderExpiryStrategy;
|
||||
expiresAt?: string;
|
||||
|
||||
oco?: boolean;
|
||||
|
||||
ocoTriggerType: 'price' | 'trailingPercentOffset';
|
||||
ocoTriggerPrice?: string;
|
||||
ocoTriggerTrailingPercentOffset?: string;
|
||||
|
||||
ocoType: OrderType;
|
||||
ocoSize: string;
|
||||
ocoTimeInForce: OrderTimeInForce;
|
||||
ocoPrice?: string;
|
||||
}
|
||||
|
||||
export type OrderFormValues = {
|
||||
@ -138,6 +149,7 @@ export const useDealTicketFormValues = create<Store>()(
|
||||
})),
|
||||
{
|
||||
name: 'vega_deal_ticket_store',
|
||||
version: 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -59,6 +59,22 @@ export const mapFormValuesToOrderSubmission = (
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const setTrigger = (
|
||||
stopOrderSetup: StopOrderSetup,
|
||||
triggerType: StopOrderFormValues['triggerPrice'],
|
||||
triggerPrice: StopOrderFormValues['triggerPrice'],
|
||||
triggerTrailingPercentOffset: StopOrderFormValues['triggerTrailingPercentOffset'],
|
||||
decimalPlaces: number
|
||||
) => {
|
||||
if (triggerType === 'price') {
|
||||
stopOrderSetup.price = removeDecimal(triggerPrice ?? '', decimalPlaces);
|
||||
} else if (triggerType === 'trailingPercentOffset') {
|
||||
stopOrderSetup.trailingPercentOffset = (
|
||||
Number(triggerTrailingPercentOffset) / 100
|
||||
).toFixed(3);
|
||||
}
|
||||
};
|
||||
|
||||
export const mapFormValuesToStopOrdersSubmission = (
|
||||
data: StopOrderFormValues,
|
||||
marketId: string,
|
||||
@ -81,31 +97,46 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
positionDecimalPlaces
|
||||
),
|
||||
};
|
||||
if (data.triggerType === 'price') {
|
||||
stopOrderSetup.price = removeDecimal(
|
||||
data.triggerPrice ?? '',
|
||||
setTrigger(
|
||||
stopOrderSetup,
|
||||
data.triggerType,
|
||||
data.triggerPrice,
|
||||
data.triggerTrailingPercentOffset,
|
||||
decimalPlaces
|
||||
);
|
||||
let oppositeStopOrderSetup: StopOrderSetup | undefined = undefined;
|
||||
if (data.oco) {
|
||||
oppositeStopOrderSetup = {
|
||||
orderSubmission: mapFormValuesToOrderSubmission(
|
||||
{
|
||||
type: data.ocoType,
|
||||
side: data.side,
|
||||
size: data.ocoSize,
|
||||
timeInForce: data.ocoTimeInForce,
|
||||
price: data.ocoPrice,
|
||||
reduceOnly: true,
|
||||
},
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces
|
||||
),
|
||||
};
|
||||
setTrigger(
|
||||
oppositeStopOrderSetup,
|
||||
data.ocoTriggerType,
|
||||
data.ocoTriggerPrice,
|
||||
data.ocoTriggerTrailingPercentOffset,
|
||||
decimalPlaces
|
||||
);
|
||||
} else if (data.triggerType === 'trailingPercentOffset') {
|
||||
stopOrderSetup.trailingPercentOffset = (
|
||||
Number(data.triggerTrailingPercentOffset) / 100
|
||||
).toFixed(3);
|
||||
}
|
||||
|
||||
if (data.expire) {
|
||||
stopOrderSetup.expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
|
||||
if (
|
||||
data.expiryStrategy ===
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||
) {
|
||||
stopOrderSetup.expiryStrategy =
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS;
|
||||
} else if (
|
||||
data.expiryStrategy ===
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||
) {
|
||||
stopOrderSetup.expiryStrategy =
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT;
|
||||
const expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
|
||||
stopOrderSetup.expiresAt = expiresAt;
|
||||
stopOrderSetup.expiryStrategy = data.expiryStrategy;
|
||||
if (oppositeStopOrderSetup) {
|
||||
oppositeStopOrderSetup.expiresAt = expiresAt;
|
||||
oppositeStopOrderSetup.expiryStrategy = data.expiryStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,12 +145,14 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
) {
|
||||
submission.risesAbove = stopOrderSetup;
|
||||
submission.fallsBelow = oppositeStopOrderSetup;
|
||||
}
|
||||
if (
|
||||
data.triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
) {
|
||||
submission.fallsBelow = stopOrderSetup;
|
||||
submission.risesAbove = oppositeStopOrderSetup;
|
||||
}
|
||||
|
||||
return submission;
|
||||
|
@ -14,8 +14,8 @@ import {
|
||||
VegaIconNames,
|
||||
DropdownMenuItem,
|
||||
TradingDropdownCopyItem,
|
||||
Pill,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import {
|
||||
AgGridLazy as AgGrid,
|
||||
@ -32,7 +32,6 @@ import type {
|
||||
VegaValueFormatterParams,
|
||||
VegaValueGetterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import type { Order } from '../order-data-provider';
|
||||
@ -50,236 +49,249 @@ export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
|
||||
isReadOnly: boolean;
|
||||
};
|
||||
|
||||
export const StopOrdersTable = memo<
|
||||
StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> }
|
||||
>(({ onCancel, onView, onMarketClick, ...props }: StopOrdersTableProps) => {
|
||||
const showAllActions = !props.isReadOnly;
|
||||
const columnDefs: ColDef[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'market.tradableInstrument.instrument.code',
|
||||
cellRenderer: 'MarketNameCell',
|
||||
cellRendererParams: { idPath: 'market.id', onMarketClick },
|
||||
},
|
||||
{
|
||||
headerName: t('Trigger'),
|
||||
field: 'trigger',
|
||||
cellClass: 'font-mono text-right',
|
||||
type: 'rightAligned',
|
||||
sortable: false,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
|
||||
data ? formatTrigger(data, data.market.decimalPlaces) : '',
|
||||
},
|
||||
{
|
||||
field: 'expiresAt',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
|
||||
if (
|
||||
data &&
|
||||
value &&
|
||||
data?.expiryStrategy !==
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
|
||||
) {
|
||||
const expiresAt = getDateTimeFormat().format(new Date(value));
|
||||
const expiryStrategy =
|
||||
data.expiryStrategy ===
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||
? t('Submit')
|
||||
: t('Cancels');
|
||||
return `${expiryStrategy} ${expiresAt}`;
|
||||
}
|
||||
return '';
|
||||
export const StopOrdersTable = memo(
|
||||
({ onCancel, onMarketClick, onView, ...props }: StopOrdersTableProps) => {
|
||||
const showAllActions = !props.isReadOnly;
|
||||
const columnDefs: ColDef[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'market.tradableInstrument.instrument.code',
|
||||
cellRenderer: 'MarketNameCell',
|
||||
cellRendererParams: { idPath: 'market.id', onMarketClick },
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Size'),
|
||||
field: 'submission.size',
|
||||
cellClass: 'font-mono text-right',
|
||||
type: 'rightAligned',
|
||||
cellClassRules: {
|
||||
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
|
||||
data?.submission.size === Schema.Side.SIDE_BUY,
|
||||
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
|
||||
data?.submission.size === Schema.Side.SIDE_SELL,
|
||||
{
|
||||
headerName: t('Trigger'),
|
||||
field: 'trigger',
|
||||
cellClass: 'font-mono text-right',
|
||||
type: 'rightAligned',
|
||||
sortable: false,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
|
||||
data ? formatTrigger(data, data.market.decimalPlaces) : '',
|
||||
},
|
||||
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
|
||||
return data?.submission.size && data.market
|
||||
? toBigNum(
|
||||
data.submission.size,
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
)
|
||||
.multipliedBy(
|
||||
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
|
||||
{
|
||||
field: 'expiresAt',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
|
||||
if (
|
||||
data &&
|
||||
value &&
|
||||
data?.expiryStrategy !==
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
|
||||
) {
|
||||
const expiresAt = getDateTimeFormat().format(new Date(value));
|
||||
const expiryStrategy =
|
||||
data.expiryStrategy ===
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||
? t('Submit')
|
||||
: t('Cancels');
|
||||
return `${expiryStrategy} ${expiresAt}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Size'),
|
||||
field: 'submission.size',
|
||||
cellClass: 'font-mono text-right',
|
||||
type: 'rightAligned',
|
||||
cellClassRules: {
|
||||
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
|
||||
data?.submission.size === Schema.Side.SIDE_BUY,
|
||||
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
|
||||
data?.submission.size === Schema.Side.SIDE_SELL,
|
||||
},
|
||||
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
|
||||
return data?.submission.size && data.market
|
||||
? toBigNum(
|
||||
data.submission.size,
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
)
|
||||
.toNumber()
|
||||
: undefined;
|
||||
.multipliedBy(
|
||||
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
|
||||
)
|
||||
.toNumber()
|
||||
: undefined;
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
if (!data?.market || !isNumeric(data.submission.size)) {
|
||||
return '-';
|
||||
}
|
||||
const prefix = data
|
||||
? data.submission.side === Schema.Side.SIDE_BUY
|
||||
? '+'
|
||||
: '-'
|
||||
: '';
|
||||
return (
|
||||
prefix +
|
||||
addDecimalsFormatNumber(
|
||||
data.submission.size,
|
||||
data.market.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
if (!data?.market || !isNumeric(data.submission.size)) {
|
||||
return '-';
|
||||
}
|
||||
const prefix = data
|
||||
? data.submission.side === Schema.Side.SIDE_BUY
|
||||
? '+'
|
||||
: '-'
|
||||
: '';
|
||||
return (
|
||||
prefix +
|
||||
addDecimalsFormatNumber(
|
||||
data.submission.size,
|
||||
data.market.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
{
|
||||
field: 'submission.type',
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.OrderTypeMapping,
|
||||
},
|
||||
cellRenderer: ({
|
||||
value,
|
||||
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
|
||||
value ? Schema.OrderTypeMapping[value] : '',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'submission.type',
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.OrderTypeMapping,
|
||||
{
|
||||
field: 'status',
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.StopOrderStatusMapping,
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
|
||||
return value ? Schema.StopOrderStatusMapping[value] : '';
|
||||
},
|
||||
cellRenderer: ({
|
||||
valueFormatted,
|
||||
data,
|
||||
}: {
|
||||
valueFormatted: string;
|
||||
data: StopOrder;
|
||||
}) => (
|
||||
<>
|
||||
<span data-testid={`order-status-${data?.id}`}>
|
||||
{valueFormatted}
|
||||
</span>
|
||||
{data.ocoLinkId && (
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={t('One Cancels the Other')}
|
||||
>
|
||||
OCO
|
||||
</Pill>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
cellRenderer: ({
|
||||
value,
|
||||
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
|
||||
value ? Schema.OrderTypeMapping[value] : '',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.StopOrderStatusMapping,
|
||||
{
|
||||
field: 'submission.price',
|
||||
type: 'rightAligned',
|
||||
cellClass: 'font-mono text-right',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
!data?.market ||
|
||||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
|
||||
!isNumeric(value)
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
|
||||
},
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
|
||||
return value ? Schema.StopOrderStatusMapping[value] : '';
|
||||
{
|
||||
field: 'submission.timeInForce',
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.OrderTimeInForceMapping,
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<StopOrder, 'submission.timeInForce'>) => {
|
||||
return value ? Schema.OrderTimeInForceCode[value] : '';
|
||||
},
|
||||
},
|
||||
cellRenderer: ({
|
||||
valueFormatted,
|
||||
data,
|
||||
}: {
|
||||
valueFormatted: string;
|
||||
data: StopOrder;
|
||||
}) => (
|
||||
<span data-testid={`order-status-${data?.id}`}>{valueFormatted}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'submission.price',
|
||||
type: 'rightAligned',
|
||||
cellClass: 'font-mono text-right',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
!data?.market ||
|
||||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
|
||||
!isNumeric(value)
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
|
||||
{
|
||||
field: 'updatedAt',
|
||||
filter: DateRangeFilter,
|
||||
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
|
||||
data?.updatedAt || data?.createdAt,
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
const value = data.updatedAt || data.createdAt;
|
||||
return (
|
||||
<span data-value={value}>
|
||||
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'submission.timeInForce',
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.OrderTimeInForceMapping,
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<StopOrder, 'submission.timeInForce'>) => {
|
||||
return value ? Schema.OrderTimeInForceCode[value] : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'updatedAt',
|
||||
filter: DateRangeFilter,
|
||||
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
|
||||
data?.updatedAt || data?.createdAt,
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
const value = data.updatedAt || data.createdAt;
|
||||
return (
|
||||
<span data-value={value}>
|
||||
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'actions',
|
||||
...COL_DEFS.actions,
|
||||
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
|
||||
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
|
||||
cellRenderer: ({ data }: { data?: StopOrder }) => {
|
||||
if (!data) return null;
|
||||
{
|
||||
colId: 'actions',
|
||||
...COL_DEFS.actions,
|
||||
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
|
||||
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
|
||||
cellRenderer: ({ data }: { data?: StopOrder }) => {
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center justify-end">
|
||||
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
|
||||
!props.isReadOnly && (
|
||||
<ButtonLink
|
||||
data-testid="cancel"
|
||||
onClick={() => onCancel(data)}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
{data.status === Schema.StopOrderStatus.STATUS_TRIGGERED &&
|
||||
data.order && (
|
||||
<ActionsDropdown data-testid="stop-order-actions-content">
|
||||
<TradingDropdownCopyItem
|
||||
value={data.order.id}
|
||||
text={t('Copy order ID')}
|
||||
/>
|
||||
<DropdownMenuItem
|
||||
key={'view-order'}
|
||||
data-testid="view-order"
|
||||
onClick={() =>
|
||||
data.order &&
|
||||
onView({ ...data.order, market: data.market })
|
||||
}
|
||||
return (
|
||||
<div className="flex gap-2 items-center justify-end">
|
||||
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
|
||||
!props.isReadOnly && (
|
||||
<ButtonLink
|
||||
data-testid="cancel"
|
||||
onClick={() => onCancel(data)}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.INFO} size={16} />
|
||||
{t('View order details')}
|
||||
</DropdownMenuItem>
|
||||
</ActionsDropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{t('Cancel')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
{data.status === Schema.StopOrderStatus.STATUS_TRIGGERED &&
|
||||
data.order && (
|
||||
<ActionsDropdown data-testid="stop-order-actions-content">
|
||||
<TradingDropdownCopyItem
|
||||
value={data.order.id}
|
||||
text={t('Copy order ID')}
|
||||
/>
|
||||
<DropdownMenuItem
|
||||
key={'view-order'}
|
||||
data-testid="view-order"
|
||||
onClick={() =>
|
||||
data.order &&
|
||||
onView({ ...data.order, market: data.market })
|
||||
}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.INFO} size={16} />
|
||||
{t('View order details')}
|
||||
</DropdownMenuItem>
|
||||
</ActionsDropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[onCancel, onMarketClick, onView, props.isReadOnly, showAllActions]
|
||||
);
|
||||
],
|
||||
[onCancel, onMarketClick, onView, props.isReadOnly, showAllActions]
|
||||
);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
defaultColDef={defaultColDef}
|
||||
columnDefs={columnDefs}
|
||||
getRowId={({ data }) => data.id}
|
||||
components={{ MarketNameCell }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<AgGrid
|
||||
defaultColDef={defaultColDef}
|
||||
columnDefs={columnDefs}
|
||||
getRowId={({ data }) => data.id}
|
||||
components={{ MarketNameCell }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import type {
|
||||
VegaStoredTxState,
|
||||
WithdrawalBusEventFieldsFragment,
|
||||
StopOrdersSubmission,
|
||||
StopOrderSetup,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
isTransferTransaction,
|
||||
@ -49,6 +50,7 @@ import {
|
||||
useOrderByIdQuery,
|
||||
useStopOrderByIdQuery,
|
||||
} from '@vegaprotocol/orders';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
import type { Side } from '@vegaprotocol/types';
|
||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||
@ -174,11 +176,15 @@ const SubmitOrderDetails = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const stopOrderSetup = data.risesAbove || data.fallsBelow;
|
||||
if (!stopOrderSetup) return null;
|
||||
const market = markets?.[stopOrderSetup?.orderSubmission.marketId];
|
||||
const SubmitStopOrderSetup = ({
|
||||
stopOrderSetup,
|
||||
triggerDirection,
|
||||
market,
|
||||
}: {
|
||||
stopOrderSetup: StopOrderSetup;
|
||||
triggerDirection: Schema.StopOrderTriggerDirection;
|
||||
market: Market;
|
||||
}) => {
|
||||
if (!market || !stopOrderSetup) return null;
|
||||
|
||||
const { price, size, side } = stopOrderSetup.orderSubmission;
|
||||
@ -191,37 +197,64 @@ const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
|
||||
__typename: 'StopOrderTrailingPercentOffset',
|
||||
};
|
||||
}
|
||||
const triggerDirection = data.risesAbove
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW;
|
||||
return (
|
||||
<p>
|
||||
<SizeAtPrice
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol,
|
||||
}}
|
||||
side={side}
|
||||
size={size}
|
||||
price={price}
|
||||
/>
|
||||
<br />
|
||||
{trigger &&
|
||||
formatTrigger(
|
||||
{
|
||||
triggerDirection,
|
||||
trigger,
|
||||
},
|
||||
market.decimalPlaces,
|
||||
''
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const marketId =
|
||||
data.fallsBelow?.orderSubmission.marketId ||
|
||||
data.risesAbove?.orderSubmission.marketId;
|
||||
const market = marketId && markets?.[marketId];
|
||||
if (!market) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Panel>
|
||||
<h4>{t('Submit stop order')}</h4>
|
||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
||||
<p>
|
||||
<SizeAtPrice
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol,
|
||||
}}
|
||||
side={side}
|
||||
size={size}
|
||||
price={price}
|
||||
{data.fallsBelow && (
|
||||
<SubmitStopOrderSetup
|
||||
stopOrderSetup={data.fallsBelow}
|
||||
triggerDirection={
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
}
|
||||
market={market}
|
||||
/>
|
||||
<br />
|
||||
{trigger &&
|
||||
formatTrigger(
|
||||
{
|
||||
triggerDirection,
|
||||
trigger,
|
||||
},
|
||||
market.decimalPlaces,
|
||||
''
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{data.risesAbove && (
|
||||
<SubmitStopOrderSetup
|
||||
stopOrderSetup={data.risesAbove}
|
||||
triggerDirection={
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
}
|
||||
market={market}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user