feat(trading): take profit and stop loss
This commit is contained in:
parent
89e2033556
commit
47ac8d10ef
@ -73,7 +73,7 @@ export const DealTicketContainer = ({
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
submit={(transaction) => create(transaction)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -0,0 +1,155 @@
|
||||
import { Controller, type Control } from 'react-hook-form';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import { toDecimal, useValidateAmount } from '@vegaprotocol/utils';
|
||||
import {
|
||||
TradingFormGroup,
|
||||
TradingInput,
|
||||
TradingInputError,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../use-t';
|
||||
import { type Side } from '@vegaprotocol/types';
|
||||
|
||||
export interface DealTicketSizeTakeProfitStopLossProps {
|
||||
control: Control<OrderFormValues>;
|
||||
market: Market;
|
||||
takeProfitError?: string;
|
||||
stopLossError?: string;
|
||||
setPrice?: string;
|
||||
takeProfit?: string;
|
||||
stopLoss?: string;
|
||||
side?: Side;
|
||||
}
|
||||
|
||||
export const DealTicketSizeTakeProfitStopLoss = ({
|
||||
control,
|
||||
market,
|
||||
takeProfitError,
|
||||
stopLossError,
|
||||
setPrice,
|
||||
takeProfit,
|
||||
stopLoss,
|
||||
side,
|
||||
}: DealTicketSizeTakeProfitStopLossProps) => {
|
||||
const t = useT();
|
||||
const validateAmount = useValidateAmount();
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
|
||||
const renderTakeProfitError = () => {
|
||||
if (takeProfitError) {
|
||||
return (
|
||||
<TradingInputError testId="deal-ticket-take-profit-error-message">
|
||||
{takeProfitError}
|
||||
</TradingInputError>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderStopLossError = () => {
|
||||
if (stopLossError) {
|
||||
return (
|
||||
<TradingInputError testId="deal-stop-loss-error-message">
|
||||
{stopLossError}
|
||||
</TradingInputError>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<TradingFormGroup
|
||||
label={
|
||||
<Tooltip
|
||||
description={
|
||||
<div>{t('The price at which you can take profit.')}</div>
|
||||
}
|
||||
>
|
||||
<span className="text-xs">{t('Take profit')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
labelFor="input-order-take-profit"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="takeProfit"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a take profit value'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Take profit cannot be lower than {{value}}', {
|
||||
value: priceStep,
|
||||
}),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'takeProfit'),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingInput
|
||||
id="input-order-take-profit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
min={priceStep}
|
||||
data-testid="order-take-profit"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
<div className="flex-0 items-center">
|
||||
<div className="flex"></div>
|
||||
<div className="flex"></div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TradingFormGroup
|
||||
label={
|
||||
<Tooltip description={<div>{t('The price for stop loss.')}</div>}>
|
||||
<span className="text-xs">{t('Stop loss')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
labelFor="input-order-stop-loss"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="stopLoss"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a value for stop loss'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Stop loss cannot be lower than {{value}}', {
|
||||
value: priceStep,
|
||||
}),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'stopLoss'),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingInput
|
||||
id="input-order-stop-loss"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
min={priceStep}
|
||||
data-testid="order-stop-loss"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
{renderTakeProfitError()}
|
||||
{renderStopLossError()}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -8,9 +8,12 @@ import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import { type OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
|
||||
import { type Transaction } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
mapFormValuesToOrderSubmission,
|
||||
mapFormValuesToTakeProfitAndStopLoss,
|
||||
} from '../../utils/map-form-values-to-submission';
|
||||
import {
|
||||
TradingInput as Input,
|
||||
TradingCheckbox as Checkbox,
|
||||
@ -77,6 +80,7 @@ import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
import { DealTicketSizeTakeProfitStopLoss } from './deal-ticket-size-tp-sl';
|
||||
|
||||
export const REDUCE_ONLY_TOOLTIP =
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||
@ -86,7 +90,7 @@ export interface DealTicketProps {
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string | null;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
submit: (order: OrderSubmission) => void;
|
||||
submit: (order: Transaction) => void;
|
||||
onDeposit: (assetId: string) => void;
|
||||
}
|
||||
|
||||
@ -184,9 +188,12 @@ export const DealTicket = ({
|
||||
const rawSize = watch('size');
|
||||
const rawPrice = watch('price');
|
||||
const iceberg = watch('iceberg');
|
||||
const tpSl = watch('tpSl');
|
||||
const peakSize = watch('peakSize');
|
||||
const expiresAt = watch('expiresAt');
|
||||
const postOnly = watch('postOnly');
|
||||
const takeProfit = watch('takeProfit');
|
||||
const stopLoss = watch('stopLoss');
|
||||
|
||||
useEffect(() => {
|
||||
const size = storedFormValues?.[dealTicketType]?.size;
|
||||
@ -382,17 +389,25 @@ export const DealTicket = ({
|
||||
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||
return;
|
||||
}
|
||||
submit(
|
||||
mapFormValuesToOrderSubmission(
|
||||
if (formValues.tpSl) {
|
||||
const batchMarketInstructions = mapFormValuesToTakeProfitAndStopLoss(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
market
|
||||
);
|
||||
submit({
|
||||
batchMarketInstructions,
|
||||
});
|
||||
}
|
||||
const orderSubmission = mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
submit({ orderSubmission });
|
||||
lastSubmitTime.current = now;
|
||||
},
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
|
||||
[market, submit]
|
||||
);
|
||||
useController({
|
||||
name: 'type',
|
||||
@ -674,40 +689,63 @@ export const DealTicket = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isLimitType && (
|
||||
{
|
||||
<>
|
||||
<div className="flex justify-between gap-2 pb-2">
|
||||
{isLimitType && (
|
||||
<Controller
|
||||
name="iceberg"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Tooltip
|
||||
description={
|
||||
<p>
|
||||
{t(
|
||||
'ICEBERG_TOOLTIP',
|
||||
'Trade only a fraction of the order size at once. After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away. For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each. Note that the full volume of the order is not hidden and is still reflected in the order book.'
|
||||
)}{' '}
|
||||
<ExternalLink href={DocsLinks?.ICEBERG_ORDERS}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>{' '}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Checkbox
|
||||
name="iceberg"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={disableIcebergCheckbox}
|
||||
label={t('Iceberg')}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="iceberg"
|
||||
name="tpSl"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Tooltip
|
||||
description={
|
||||
<p>
|
||||
{t(
|
||||
'ICEBERG_TOOLTIP',
|
||||
'Trade only a fraction of the order size at once. After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away. For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each. Note that the full volume of the order is not hidden and is still reflected in the order book.'
|
||||
)}{' '}
|
||||
<ExternalLink href={DocsLinks?.ICEBERG_ORDERS}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>{' '}
|
||||
</p>
|
||||
<p>{t('TP_SL_TOOLTIP', 'Take profit / Stop loss')}</p>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Checkbox
|
||||
name="iceberg"
|
||||
name="tpSl"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={disableIcebergCheckbox}
|
||||
label={t('Iceberg')}
|
||||
disabled={false}
|
||||
label={t('TP / SL')}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{iceberg && (
|
||||
{isLimitType && iceberg && (
|
||||
<DealTicketSizeIceberg
|
||||
market={market}
|
||||
peakSizeError={errors.peakSize?.message}
|
||||
@ -717,8 +755,21 @@ export const DealTicket = ({
|
||||
peakSize={peakSize}
|
||||
/>
|
||||
)}
|
||||
{tpSl && (
|
||||
<DealTicketSizeTakeProfitStopLoss
|
||||
market={market}
|
||||
takeProfitError={errors.takeProfit?.message}
|
||||
stopLossError={errors.stopLoss?.message}
|
||||
control={control}
|
||||
setPrice={price}
|
||||
takeProfit={takeProfit}
|
||||
stopLoss={stopLoss}
|
||||
side={side}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
}
|
||||
|
||||
<SummaryMessage
|
||||
error={summaryError}
|
||||
asset={asset}
|
||||
|
@ -53,6 +53,9 @@ export type OrderFormValues = {
|
||||
iceberg?: boolean;
|
||||
peakSize?: string;
|
||||
minimumVisibleSize?: string;
|
||||
tpSl?: boolean;
|
||||
takeProfit?: string;
|
||||
stopLoss?: string;
|
||||
};
|
||||
|
||||
type UpdateOrder = (marketId: string, values: Partial<OrderFormValues>) => void;
|
||||
|
@ -10,6 +10,7 @@ import type {
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { isPersistentOrder } from './time-in-force-persistence';
|
||||
import { type MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||
|
||||
export const mapFormValuesToOrderSubmission = (
|
||||
order: OrderFormValues,
|
||||
@ -159,3 +160,75 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
|
||||
return submission;
|
||||
};
|
||||
|
||||
export const mapFormValuesToTakeProfitAndStopLoss = (
|
||||
formValues: OrderFormValues,
|
||||
market: MarketFieldsFragment
|
||||
) => {
|
||||
const ocoType =
|
||||
formValues.type === Schema.OrderType.TYPE_LIMIT
|
||||
? Schema.OrderType.TYPE_MARKET
|
||||
: Schema.OrderType.TYPE_LIMIT;
|
||||
const orderSubmission = mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
|
||||
const takeProfitStopOrderSubmission =
|
||||
formValues.takeProfit &&
|
||||
mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
price: formValues.takeProfit,
|
||||
triggerDirection:
|
||||
formValues.side === Schema.Side.SIDE_SELL
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||
triggerType: 'price',
|
||||
ocoTriggerType: 'price',
|
||||
expire: false,
|
||||
ocoType,
|
||||
ocoSize: formValues.size,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
|
||||
const stopLossStopOrderSubmission =
|
||||
formValues.stopLoss &&
|
||||
mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
price: formValues.stopLoss,
|
||||
triggerDirection:
|
||||
formValues.side === Schema.Side.SIDE_BUY
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||
triggerType: 'price',
|
||||
ocoTriggerType: 'price',
|
||||
expire: false,
|
||||
ocoType,
|
||||
ocoSize: formValues.size,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
const stopOrdersSubmission = [];
|
||||
if (takeProfitStopOrderSubmission) {
|
||||
stopOrdersSubmission.push(takeProfitStopOrderSubmission);
|
||||
}
|
||||
if (stopLossStopOrderSubmission) {
|
||||
stopOrdersSubmission.push(stopLossStopOrderSubmission);
|
||||
}
|
||||
const batchMarketInstructions = {
|
||||
submissions: [orderSubmission],
|
||||
stopOrdersSubmission,
|
||||
};
|
||||
return batchMarketInstructions;
|
||||
};
|
||||
|
@ -403,6 +403,9 @@ export interface BatchMarketInstructionSubmissionBody {
|
||||
// Note: If multiple orders are submitted the first order ID is determined by hashing the signature of the transaction
|
||||
// (see determineId function). For each subsequent order's ID, a hash of the previous orders ID is used
|
||||
submissions?: OrderSubmission[];
|
||||
stopOrdersSubmission?: StopOrdersSubmission[];
|
||||
stopOrdersCancellation?: StopOrdersCancellation[];
|
||||
updateMarginMode?: UpdateMarginMode[];
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user