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(orderSizeField).clear().type('1');
|
||||||
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
||||||
cy.getByTestId(placeOrderBtn).click();
|
cy.getByTestId(placeOrderBtn).click();
|
||||||
cy.getByTestId('deal-ticket-error-message-price-limit').should(
|
cy.getByTestId('deal-ticket-error-message-price').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
'Price accepts up to 5 decimal places'
|
'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');
|
cy.getByTestId(orderSizeField).clear().type('1.234');
|
||||||
// 7002-SORD-060
|
// 7002-SORD-060
|
||||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
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',
|
'have.text',
|
||||||
'Size must be whole numbers for this market'
|
'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 () {
|
it('must warn if order size is set to 0', function () {
|
||||||
cy.getByTestId(orderSizeField).clear().type('0');
|
cy.getByTestId(orderSizeField).clear().type('0');
|
||||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
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',
|
'have.text',
|
||||||
'Size cannot be lower than 1'
|
'Size cannot be lower than 1'
|
||||||
);
|
);
|
||||||
|
@ -36,6 +36,6 @@ export const StopOrdersContainer = () => {
|
|||||||
|
|
||||||
const useStopOrdersStore = create<DataGridSlice>()(
|
const useStopOrdersStore = create<DataGridSlice>()(
|
||||||
persist(createDataGridSlice, {
|
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%);
|
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
|
||||||
background-color: #999;
|
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 sizeErrorMessage = 'stop-order-error-message-size';
|
||||||
const priceErrorMessage = 'stop-order-error-message-price';
|
const priceErrorMessage = 'stop-order-error-message-price';
|
||||||
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
|
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
|
||||||
|
const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
|
||||||
const triggerTrailingPercentOffsetErrorMessage =
|
const triggerTrailingPercentOffsetErrorMessage =
|
||||||
'stop-order-error-message-trigger-trailing-percent-offset';
|
'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 () => {
|
it('should use local storage state for initial values', async () => {
|
||||||
const values: Partial<StopOrderFormValues> = {
|
const values: Partial<StopOrderFormValues> = {
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
type: Schema.OrderType.TYPE_LIMIT,
|
||||||
@ -203,8 +196,8 @@ describe('StopOrder', () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByTestId(submitButton));
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
// price error message should not show if size has error
|
// price error message should not show if size has error
|
||||||
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
// expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
// await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
@ -249,10 +242,17 @@ describe('StopOrder', () => {
|
|||||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
|
||||||
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
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.clear(screen.getByTestId(triggerPriceInput));
|
||||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
|
||||||
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
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 () => {
|
it('validates trigger trailing percentage offset field', async () => {
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
import { useRef, useCallback, useEffect } from 'react';
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||||
import {
|
import { removeDecimal, toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||||
formatNumber,
|
import type { Control, UseFormWatch } from 'react-hook-form';
|
||||||
removeDecimal,
|
|
||||||
toDecimal,
|
|
||||||
validateAmount,
|
|
||||||
} from '@vegaprotocol/utils';
|
|
||||||
import { useForm, Controller, useController } from 'react-hook-form';
|
import { useForm, Controller, useController } from 'react-hook-form';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import {
|
import {
|
||||||
TradingRadio,
|
TradingRadio as Radio,
|
||||||
TradingRadioGroup,
|
TradingRadioGroup as RadioGroup,
|
||||||
TradingInput,
|
TradingInput as Input,
|
||||||
TradingCheckbox,
|
TradingCheckbox as Checkbox,
|
||||||
TradingFormGroup,
|
TradingFormGroup as FormGroup,
|
||||||
TradingInputError,
|
TradingInputError as InputError,
|
||||||
TradingSelect,
|
TradingSelect as Select,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
||||||
@ -49,6 +45,8 @@ export interface StopOrderProps {
|
|||||||
submit: (order: StopOrdersSubmission) => void;
|
submit: (order: StopOrdersSubmission) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trailingPercentOffsetStep = '0.1';
|
||||||
|
|
||||||
const getDefaultValues = (
|
const getDefaultValues = (
|
||||||
type: Schema.OrderType,
|
type: Schema.OrderType,
|
||||||
storedValues?: Partial<StopOrderFormValues>
|
storedValues?: Partial<StopOrderFormValues>
|
||||||
@ -62,9 +60,425 @@ const getDefaultValues = (
|
|||||||
expire: false,
|
expire: false,
|
||||||
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||||
size: '0',
|
size: '0',
|
||||||
|
ocoType: type,
|
||||||
|
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
|
ocoTriggerType: 'price',
|
||||||
|
ocoSize: '0',
|
||||||
...storedValues,
|
...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) => {
|
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const setType = useDealTicketFormValues((state) => state.setType);
|
const setType = useDealTicketFormValues((state) => state.setType);
|
||||||
@ -107,6 +521,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
const timeInForce = watch('timeInForce');
|
const timeInForce = watch('timeInForce');
|
||||||
const rawPrice = watch('price');
|
const rawPrice = watch('price');
|
||||||
const rawSize = watch('size');
|
const rawSize = watch('size');
|
||||||
|
const oco = watch('oco');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const size = storedFormValues?.[dealTicketType]?.size;
|
const size = storedFormValues?.[dealTicketType]?.size;
|
||||||
@ -155,12 +570,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
|
|
||||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
const priceStep = toDecimal(market?.decimalPlaces);
|
const priceStep = toDecimal(market?.decimalPlaces);
|
||||||
const trailingPercentOffsetStep = '0.1';
|
|
||||||
|
|
||||||
const priceFormatted =
|
|
||||||
isPriceTrigger && triggerPrice
|
|
||||||
? formatNumber(triggerPrice, market.decimalPlaces)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
useController({
|
useController({
|
||||||
name: 'type',
|
name: 'type',
|
||||||
@ -187,9 +596,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{errors.type && (
|
{errors.type && (
|
||||||
<TradingInputError testId="stop-order-error-message-type">
|
<InputError testId="stop-order-error-message-type">
|
||||||
{errors.type.message}
|
{errors.type.message}
|
||||||
</TradingInputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
@ -199,302 +608,117 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
<SideSelector value={field.value} onValueChange={field.onChange} />
|
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<TradingFormGroup label={t('Trigger')} compact={true} labelFor="">
|
<Trigger
|
||||||
<Controller
|
|
||||||
name="triggerDirection"
|
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => {
|
watch={watch}
|
||||||
const { onChange, value } = field;
|
priceStep={priceStep}
|
||||||
return (
|
assetSymbol={asset.symbol}
|
||||||
<TradingRadioGroup
|
marketPrice={marketPrice}
|
||||||
name="triggerDirection"
|
decimalPlaces={market.decimalPlaces}
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
orientation="horizontal"
|
|
||||||
className="mb-2"
|
|
||||||
>
|
|
||||||
<TradingRadio
|
|
||||||
value={
|
|
||||||
Schema.StopOrderTriggerDirection
|
|
||||||
.TRIGGER_DIRECTION_RISES_ABOVE
|
|
||||||
}
|
|
||||||
id="triggerDirection-risesAbove"
|
|
||||||
label={'Rises above'}
|
|
||||||
/>
|
/>
|
||||||
<TradingRadio
|
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
|
||||||
value={
|
<Price
|
||||||
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}
|
control={control}
|
||||||
render={({ field, fieldState }) => {
|
watch={watch}
|
||||||
const { value, ...props } = field;
|
priceStep={priceStep}
|
||||||
return (
|
quoteName={quoteName}
|
||||||
<div className="mb-2">
|
|
||||||
<TradingInput
|
|
||||||
data-testid="triggerPrice"
|
|
||||||
type="number"
|
|
||||||
step={priceStep}
|
|
||||||
appendElement={asset.symbol}
|
|
||||||
value={value || ''}
|
|
||||||
hasError={!!fieldState.error}
|
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
|
<Size control={control} sizeStep={sizeStep} />
|
||||||
|
<TimeInForce control={control} />
|
||||||
|
<div className="flex gap-2 pb-3 justify-end">
|
||||||
|
<ReduceOnly />
|
||||||
</div>
|
</div>
|
||||||
);
|
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 pb-2 justify-between">
|
<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
|
<Controller
|
||||||
name="expire"
|
name="expire"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
const { onChange: onCheckedChange, value } = field;
|
const { onChange: onCheckedChange, value } = field;
|
||||||
return (
|
return (
|
||||||
<TradingCheckbox
|
<Checkbox
|
||||||
onCheckedChange={onCheckedChange}
|
onCheckedChange={onCheckedChange}
|
||||||
checked={value}
|
checked={value}
|
||||||
name="expire"
|
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>
|
</div>
|
||||||
{expire && (
|
{expire && (
|
||||||
<>
|
<>
|
||||||
<TradingFormGroup
|
<FormGroup label={t('Strategy')} labelFor="expiryStrategy">
|
||||||
label={t('Strategy')}
|
|
||||||
labelFor="expiryStrategy"
|
|
||||||
compact={true}
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
name="expiryStrategy"
|
name="expiryStrategy"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
|
const { onChange, value } = field;
|
||||||
return (
|
return (
|
||||||
<TradingRadioGroup orientation="horizontal" {...field}>
|
<RadioGroup
|
||||||
<TradingRadio
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
disabled={oco}
|
||||||
value={
|
value={
|
||||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||||
}
|
}
|
||||||
id="expiryStrategy-submit"
|
id="expiryStrategy-submit"
|
||||||
label={'Submit'}
|
label={'Submit'}
|
||||||
/>
|
/>
|
||||||
<TradingRadio
|
<Radio
|
||||||
value={
|
value={
|
||||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||||
}
|
}
|
||||||
id="expiryStrategy-cancel"
|
id="expiryStrategy-cancel"
|
||||||
label={'Cancel'}
|
label={'Cancel'}
|
||||||
/>
|
/>
|
||||||
</TradingRadioGroup>
|
</RadioGroup>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TradingFormGroup>
|
</FormGroup>
|
||||||
<div className="mb-2">
|
<div className="mb-4">
|
||||||
<Controller
|
<Controller
|
||||||
name="expiresAt"
|
name="expiresAt"
|
||||||
control={control}
|
control={control}
|
||||||
|
@ -7,7 +7,6 @@ import { DealTicket } from './deal-ticket';
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { addDecimal } from '@vegaprotocol/utils';
|
|
||||||
import type { OrdersQuery } from '@vegaprotocol/orders';
|
import type { OrdersQuery } from '@vegaprotocol/orders';
|
||||||
import {
|
import {
|
||||||
DealTicketType,
|
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', () => {
|
it('should use local storage state for initial values', () => {
|
||||||
const expectedOrder = {
|
const expectedOrder = {
|
||||||
marketId: market.id,
|
marketId: market.id,
|
||||||
|
@ -3,7 +3,6 @@ import * as Schema from '@vegaprotocol/types';
|
|||||||
import type { FormEventHandler } from 'react';
|
import type { FormEventHandler } from 'react';
|
||||||
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
|
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||||
import { Controller, useController, useForm } from 'react-hook-form';
|
import { Controller, useController, useForm } from 'react-hook-form';
|
||||||
import { DealTicketAmount } from './deal-ticket-amount';
|
|
||||||
import { DealTicketButton } from './deal-ticket-button';
|
import { DealTicketButton } from './deal-ticket-button';
|
||||||
import {
|
import {
|
||||||
DealTicketFeeDetails,
|
DealTicketFeeDetails,
|
||||||
@ -17,8 +16,10 @@ import type { OrderSubmission } from '@vegaprotocol/wallet';
|
|||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
|
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
|
||||||
import {
|
import {
|
||||||
TradingCheckbox,
|
TradingInput as Input,
|
||||||
TradingInputError,
|
TradingCheckbox as Checkbox,
|
||||||
|
TradingFormGroup as FormGroup,
|
||||||
|
TradingInputError as InputError,
|
||||||
Intent,
|
Intent,
|
||||||
Notification,
|
Notification,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -28,7 +29,12 @@ import {
|
|||||||
useEstimatePositionQuery,
|
useEstimatePositionQuery,
|
||||||
useOpenVolume,
|
useOpenVolume,
|
||||||
} from '@vegaprotocol/positions';
|
} from '@vegaprotocol/positions';
|
||||||
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
import {
|
||||||
|
toBigNum,
|
||||||
|
removeDecimal,
|
||||||
|
validateAmount,
|
||||||
|
toDecimal,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||||
import { getDerivedPrice } from '@vegaprotocol/markets';
|
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||||
import type { OrderInfo } from '@vegaprotocol/types';
|
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 (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={
|
onSubmit={
|
||||||
@ -366,15 +376,82 @@ export const DealTicket = ({
|
|||||||
<SideSelector value={field.value} onValueChange={field.onChange} />
|
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DealTicketAmount
|
|
||||||
type={type}
|
<Controller
|
||||||
|
name="size"
|
||||||
control={control}
|
control={control}
|
||||||
market={market}
|
rules={{
|
||||||
marketData={marketData}
|
required: t('You need to provide a size'),
|
||||||
marketPrice={marketPrice || undefined}
|
min: {
|
||||||
sizeError={errors.size?.message}
|
value: sizeStep,
|
||||||
priceError={errors.price?.message}
|
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
|
<Controller
|
||||||
name="timeInForce"
|
name="timeInForce"
|
||||||
control={control}
|
control={control}
|
||||||
@ -417,7 +494,7 @@ export const DealTicket = ({
|
|||||||
name="postOnly"
|
name="postOnly"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TradingCheckbox
|
<Checkbox
|
||||||
name="post-only"
|
name="post-only"
|
||||||
checked={!disablePostOnlyCheckbox && field.value}
|
checked={!disablePostOnlyCheckbox && field.value}
|
||||||
disabled={disablePostOnlyCheckbox}
|
disabled={disablePostOnlyCheckbox}
|
||||||
@ -449,7 +526,7 @@ export const DealTicket = ({
|
|||||||
name="reduceOnly"
|
name="reduceOnly"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TradingCheckbox
|
<Checkbox
|
||||||
name="reduce-only"
|
name="reduce-only"
|
||||||
checked={!disableReduceOnlyCheckbox && field.value}
|
checked={!disableReduceOnlyCheckbox && field.value}
|
||||||
disabled={disableReduceOnlyCheckbox}
|
disabled={disableReduceOnlyCheckbox}
|
||||||
@ -483,7 +560,7 @@ export const DealTicket = ({
|
|||||||
name="iceberg"
|
name="iceberg"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TradingCheckbox
|
<Checkbox
|
||||||
name="iceberg"
|
name="iceberg"
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
@ -572,11 +649,11 @@ export const NoWalletWarning = ({
|
|||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<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'
|
'You need to connect your own wallet to start trading on this market'
|
||||||
}
|
}
|
||||||
</TradingInputError>
|
</InputError>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -613,9 +690,9 @@ const SummaryMessage = memo(
|
|||||||
if (error?.message) {
|
if (error?.message) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<TradingInputError testId="deal-ticket-error-message-summary">
|
<InputError testId="deal-ticket-error-message-summary">
|
||||||
{error?.message}
|
{error?.message}
|
||||||
</TradingInputError>
|
</InputError>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,11 @@ export const ExpirySelector = ({
|
|||||||
const dateFormatted = formatForInput(date);
|
const dateFormatted = formatForInput(date);
|
||||||
const minDate = formatForInput(date);
|
const minDate = formatForInput(date);
|
||||||
return (
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
<TradingFormGroup
|
<TradingFormGroup
|
||||||
label={t('Expiry time/date')}
|
label={t('Expiry time/date')}
|
||||||
labelFor="expiration"
|
labelFor="expiration"
|
||||||
compact={true}
|
compact
|
||||||
>
|
>
|
||||||
<TradingInput
|
<TradingInput
|
||||||
data-testid="date-picker-field"
|
data-testid="date-picker-field"
|
||||||
@ -43,5 +44,6 @@ export const ExpirySelector = ({
|
|||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
export * from './deal-ticket-amount';
|
|
||||||
export * from './deal-ticket-container';
|
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';
|
||||||
export * from './deal-ticket-stop-order';
|
export * from './deal-ticket-stop-order';
|
||||||
export * from './deal-ticket-container';
|
export * from './deal-ticket-container';
|
||||||
|
@ -90,6 +90,7 @@ export const TimeInForceSelector = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
<TradingFormGroup
|
<TradingFormGroup
|
||||||
label={t('Time in force')}
|
label={t('Time in force')}
|
||||||
labelFor="select-time-in-force"
|
labelFor="select-time-in-force"
|
||||||
@ -117,5 +118,6 @@ export const TimeInForceSelector = ({
|
|||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -76,7 +76,7 @@ export const TypeToggle = ({
|
|||||||
<TradingDropdownTrigger
|
<TradingDropdownTrigger
|
||||||
data-testid="order-type-Stop"
|
data-testid="order-type-Stop"
|
||||||
className={classNames(
|
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,
|
'bg-vega-clight-500 dark:bg-vega-cdark-500': selectedOption,
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,17 @@ export interface StopOrderFormValues {
|
|||||||
expire: boolean;
|
expire: boolean;
|
||||||
expiryStrategy?: Schema.StopOrderExpiryStrategy;
|
expiryStrategy?: Schema.StopOrderExpiryStrategy;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
|
|
||||||
|
oco?: boolean;
|
||||||
|
|
||||||
|
ocoTriggerType: 'price' | 'trailingPercentOffset';
|
||||||
|
ocoTriggerPrice?: string;
|
||||||
|
ocoTriggerTrailingPercentOffset?: string;
|
||||||
|
|
||||||
|
ocoType: OrderType;
|
||||||
|
ocoSize: string;
|
||||||
|
ocoTimeInForce: OrderTimeInForce;
|
||||||
|
ocoPrice?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderFormValues = {
|
export type OrderFormValues = {
|
||||||
@ -138,6 +149,7 @@ export const useDealTicketFormValues = create<Store>()(
|
|||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: 'vega_deal_ticket_store',
|
name: 'vega_deal_ticket_store',
|
||||||
|
version: 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -59,6 +59,22 @@ export const mapFormValuesToOrderSubmission = (
|
|||||||
: undefined,
|
: 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 = (
|
export const mapFormValuesToStopOrdersSubmission = (
|
||||||
data: StopOrderFormValues,
|
data: StopOrderFormValues,
|
||||||
marketId: string,
|
marketId: string,
|
||||||
@ -81,31 +97,46 @@ export const mapFormValuesToStopOrdersSubmission = (
|
|||||||
positionDecimalPlaces
|
positionDecimalPlaces
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
if (data.triggerType === 'price') {
|
setTrigger(
|
||||||
stopOrderSetup.price = removeDecimal(
|
stopOrderSetup,
|
||||||
data.triggerPrice ?? '',
|
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
|
decimalPlaces
|
||||||
);
|
);
|
||||||
} else if (data.triggerType === 'trailingPercentOffset') {
|
|
||||||
stopOrderSetup.trailingPercentOffset = (
|
|
||||||
Number(data.triggerTrailingPercentOffset) / 100
|
|
||||||
).toFixed(3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.expire) {
|
if (data.expire) {
|
||||||
stopOrderSetup.expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
|
const expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
|
||||||
if (
|
stopOrderSetup.expiresAt = expiresAt;
|
||||||
data.expiryStrategy ===
|
stopOrderSetup.expiryStrategy = data.expiryStrategy;
|
||||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
if (oppositeStopOrderSetup) {
|
||||||
) {
|
oppositeStopOrderSetup.expiresAt = expiresAt;
|
||||||
stopOrderSetup.expiryStrategy =
|
oppositeStopOrderSetup.expiryStrategy = data.expiryStrategy;
|
||||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS;
|
|
||||||
} else if (
|
|
||||||
data.expiryStrategy ===
|
|
||||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
|
||||||
) {
|
|
||||||
stopOrderSetup.expiryStrategy =
|
|
||||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,12 +145,14 @@ export const mapFormValuesToStopOrdersSubmission = (
|
|||||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||||
) {
|
) {
|
||||||
submission.risesAbove = stopOrderSetup;
|
submission.risesAbove = stopOrderSetup;
|
||||||
|
submission.fallsBelow = oppositeStopOrderSetup;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
data.triggerDirection ===
|
data.triggerDirection ===
|
||||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||||
) {
|
) {
|
||||||
submission.fallsBelow = stopOrderSetup;
|
submission.fallsBelow = stopOrderSetup;
|
||||||
|
submission.risesAbove = oppositeStopOrderSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
return submission;
|
return submission;
|
||||||
|
@ -14,8 +14,8 @@ import {
|
|||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
TradingDropdownCopyItem,
|
TradingDropdownCopyItem,
|
||||||
|
Pill,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { ForwardedRef } from 'react';
|
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
AgGridLazy as AgGrid,
|
AgGridLazy as AgGrid,
|
||||||
@ -32,7 +32,6 @@ import type {
|
|||||||
VegaValueFormatterParams,
|
VegaValueFormatterParams,
|
||||||
VegaValueGetterParams,
|
VegaValueGetterParams,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
|
||||||
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
|
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
|
||||||
import type { ColDef } from 'ag-grid-community';
|
import type { ColDef } from 'ag-grid-community';
|
||||||
import type { Order } from '../order-data-provider';
|
import type { Order } from '../order-data-provider';
|
||||||
@ -50,9 +49,8 @@ export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
|
|||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StopOrdersTable = memo<
|
export const StopOrdersTable = memo(
|
||||||
StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> }
|
({ onCancel, onMarketClick, onView, ...props }: StopOrdersTableProps) => {
|
||||||
>(({ onCancel, onView, onMarketClick, ...props }: StopOrdersTableProps) => {
|
|
||||||
const showAllActions = !props.isReadOnly;
|
const showAllActions = !props.isReadOnly;
|
||||||
const columnDefs: ColDef[] = useMemo(
|
const columnDefs: ColDef[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -171,7 +169,20 @@ export const StopOrdersTable = memo<
|
|||||||
valueFormatted: string;
|
valueFormatted: string;
|
||||||
data: StopOrder;
|
data: StopOrder;
|
||||||
}) => (
|
}) => (
|
||||||
<span data-testid={`order-status-${data?.id}`}>{valueFormatted}</span>
|
<>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -282,4 +293,5 @@ export const StopOrdersTable = memo<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
@ -9,6 +9,7 @@ import type {
|
|||||||
VegaStoredTxState,
|
VegaStoredTxState,
|
||||||
WithdrawalBusEventFieldsFragment,
|
WithdrawalBusEventFieldsFragment,
|
||||||
StopOrdersSubmission,
|
StopOrdersSubmission,
|
||||||
|
StopOrderSetup,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import {
|
import {
|
||||||
isTransferTransaction,
|
isTransferTransaction,
|
||||||
@ -49,6 +50,7 @@ import {
|
|||||||
useOrderByIdQuery,
|
useOrderByIdQuery,
|
||||||
useStopOrderByIdQuery,
|
useStopOrderByIdQuery,
|
||||||
} from '@vegaprotocol/orders';
|
} from '@vegaprotocol/orders';
|
||||||
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||||
import type { Side } from '@vegaprotocol/types';
|
import type { Side } from '@vegaprotocol/types';
|
||||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||||
@ -174,11 +176,15 @@ const SubmitOrderDetails = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
|
const SubmitStopOrderSetup = ({
|
||||||
const { data: markets } = useMarketsMapProvider();
|
stopOrderSetup,
|
||||||
const stopOrderSetup = data.risesAbove || data.fallsBelow;
|
triggerDirection,
|
||||||
if (!stopOrderSetup) return null;
|
market,
|
||||||
const market = markets?.[stopOrderSetup?.orderSubmission.marketId];
|
}: {
|
||||||
|
stopOrderSetup: StopOrderSetup;
|
||||||
|
triggerDirection: Schema.StopOrderTriggerDirection;
|
||||||
|
market: Market;
|
||||||
|
}) => {
|
||||||
if (!market || !stopOrderSetup) return null;
|
if (!market || !stopOrderSetup) return null;
|
||||||
|
|
||||||
const { price, size, side } = stopOrderSetup.orderSubmission;
|
const { price, size, side } = stopOrderSetup.orderSubmission;
|
||||||
@ -191,21 +197,14 @@ const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
|
|||||||
__typename: 'StopOrderTrailingPercentOffset',
|
__typename: 'StopOrderTrailingPercentOffset',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const triggerDirection = data.risesAbove
|
|
||||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
|
||||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW;
|
|
||||||
return (
|
return (
|
||||||
<Panel>
|
|
||||||
<h4>{t('Submit stop order')}</h4>
|
|
||||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
|
||||||
<p>
|
<p>
|
||||||
<SizeAtPrice
|
<SizeAtPrice
|
||||||
meta={{
|
meta={{
|
||||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||||
decimalPlaces: market.decimalPlaces,
|
decimalPlaces: market.decimalPlaces,
|
||||||
asset:
|
asset:
|
||||||
market.tradableInstrument.instrument.product.settlementAsset
|
market.tradableInstrument.instrument.product.settlementAsset.symbol,
|
||||||
.symbol,
|
|
||||||
}}
|
}}
|
||||||
side={side}
|
side={side}
|
||||||
size={size}
|
size={size}
|
||||||
@ -222,6 +221,40 @@ const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
|
|||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
</p>
|
</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>
|
||||||
|
{data.fallsBelow && (
|
||||||
|
<SubmitStopOrderSetup
|
||||||
|
stopOrderSetup={data.fallsBelow}
|
||||||
|
triggerDirection={
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||||
|
}
|
||||||
|
market={market}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{data.risesAbove && (
|
||||||
|
<SubmitStopOrderSetup
|
||||||
|
stopOrderSetup={data.risesAbove}
|
||||||
|
triggerDirection={
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||||
|
}
|
||||||
|
market={market}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user