feat(trading): take profit and stop loss (#5902)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
This commit is contained in:
parent
464d5af6be
commit
93643f1737
@ -79,21 +79,21 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have information on active nodes', function () {
|
||||
it.skip('should have information on active nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.first()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'active nodes');
|
||||
});
|
||||
|
||||
it('should have information on consensus nodes', function () {
|
||||
it.skip('should have information on consensus nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.last()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'consensus nodes');
|
||||
});
|
||||
|
||||
it('should contain link to specific validators', function () {
|
||||
it.skip('should contain link to specific validators', function () {
|
||||
cy.getByTestId('validators')
|
||||
.should('have.length', '2')
|
||||
.each(($validator) => {
|
||||
|
@ -73,7 +73,7 @@ export const DealTicketContainer = ({
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
submit={(transaction) => create(transaction)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -0,0 +1,172 @@
|
||||
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,
|
||||
TradingInputError,
|
||||
Tooltip,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
Pill,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export interface DealTicketPriceTakeProfitStopLossProps {
|
||||
control: Control<OrderFormValues>;
|
||||
market: Market;
|
||||
takeProfitError?: string;
|
||||
stopLossError?: string;
|
||||
quoteName?: string;
|
||||
}
|
||||
|
||||
export const DealTicketPriceTakeProfitStopLoss = ({
|
||||
control,
|
||||
market,
|
||||
takeProfitError,
|
||||
stopLossError,
|
||||
quoteName,
|
||||
}: DealTicketPriceTakeProfitStopLossProps) => {
|
||||
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 flex-col gap-2">
|
||||
<div className="flex-1">
|
||||
<TradingFormGroup
|
||||
label={
|
||||
<Tooltip
|
||||
description={<div>{t('The price for 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={{
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t(
|
||||
'Take profit price cannot be lower than {{priceStep}}',
|
||||
{
|
||||
priceStep,
|
||||
}
|
||||
),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'takeProfit'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-2">
|
||||
<FormGroup
|
||||
labelFor="input-price-take-profit"
|
||||
label={''}
|
||||
compact
|
||||
>
|
||||
<Input
|
||||
id="input-price-take-profit"
|
||||
appendElement={<Pill size="xs">{quoteName}</Pill>}
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price-take-profit"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId="deal-ticket-error-message-price-take-profit">
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</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={{
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than {{priceStep}}', {
|
||||
priceStep,
|
||||
}),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'stopLoss'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-2">
|
||||
<FormGroup
|
||||
labelFor="input-price-stop-loss"
|
||||
label={''}
|
||||
compact
|
||||
>
|
||||
<Input
|
||||
id="input-price-stop-loss"
|
||||
appendElement={<Pill size="xs">{quoteName}</Pill>}
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price-stop-loss"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId="deal-ticket-error-message-price-stop-loss">
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
{renderTakeProfitError()}
|
||||
{renderStopLossError()}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1003,7 +1003,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
name="oco"
|
||||
label={
|
||||
<Tooltip
|
||||
description={<span>{t('One cancels another')}</span>}
|
||||
description={<span>{t('One cancels the other')}</span>}
|
||||
>
|
||||
<>{t('OCO')}</>
|
||||
</Tooltip>
|
||||
|
@ -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,8 @@ import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
import { DealTicketPriceTakeProfitStopLoss } from './deal-ticket-price-tp-sl';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
|
||||
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 +91,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,6 +189,7 @@ 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');
|
||||
@ -382,17 +388,28 @@ export const DealTicket = ({
|
||||
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||
return;
|
||||
}
|
||||
submit(
|
||||
mapFormValuesToOrderSubmission(
|
||||
if (formValues.tpSl) {
|
||||
const reference = `${pubKey}-${now}-${uniqueId()}`;
|
||||
const batchMarketInstructions = mapFormValuesToTakeProfitAndStopLoss(
|
||||
formValues,
|
||||
market,
|
||||
reference
|
||||
);
|
||||
submit({
|
||||
batchMarketInstructions,
|
||||
});
|
||||
} else {
|
||||
const orderSubmission = mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
);
|
||||
submit({ orderSubmission });
|
||||
}
|
||||
lastSubmitTime.current = now;
|
||||
},
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
|
||||
[market, pubKey, submit]
|
||||
);
|
||||
useController({
|
||||
name: 'type',
|
||||
@ -674,40 +691,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 +757,18 @@ export const DealTicket = ({
|
||||
peakSize={peakSize}
|
||||
/>
|
||||
)}
|
||||
{tpSl && (
|
||||
<DealTicketPriceTakeProfitStopLoss
|
||||
market={market}
|
||||
takeProfitError={errors.takeProfit?.message}
|
||||
stopLossError={errors.stopLoss?.message}
|
||||
control={control}
|
||||
quoteName={quoteName}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
}
|
||||
|
||||
<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,13 +10,16 @@ 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,
|
||||
marketId: string,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
positionDecimalPlaces: number,
|
||||
reference?: string
|
||||
): OrderSubmission => ({
|
||||
reference,
|
||||
marketId: marketId,
|
||||
type: order.type,
|
||||
side: order.side,
|
||||
@ -81,7 +84,8 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
data: StopOrderFormValues,
|
||||
marketId: string,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
positionDecimalPlaces: number,
|
||||
reference?: string
|
||||
): StopOrdersSubmission => {
|
||||
const submission: StopOrdersSubmission = {};
|
||||
const stopOrderSetup: StopOrderSetup = {
|
||||
@ -96,7 +100,8 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
},
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces
|
||||
positionDecimalPlaces,
|
||||
reference
|
||||
),
|
||||
};
|
||||
setTrigger(
|
||||
@ -120,7 +125,8 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
},
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces
|
||||
positionDecimalPlaces,
|
||||
reference
|
||||
),
|
||||
};
|
||||
setTrigger(
|
||||
@ -159,3 +165,125 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
|
||||
return submission;
|
||||
};
|
||||
|
||||
export const mapFormValuesToTakeProfitAndStopLoss = (
|
||||
formValues: OrderFormValues,
|
||||
market: MarketFieldsFragment,
|
||||
reference: string
|
||||
) => {
|
||||
const orderSubmission = mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
|
||||
const oppositeSide =
|
||||
formValues.side === Schema.Side.SIDE_BUY
|
||||
? Schema.Side.SIDE_SELL
|
||||
: Schema.Side.SIDE_BUY;
|
||||
// For direction it needs to be implied
|
||||
// If position is LONG (BUY)
|
||||
// TP is SHORT and trigger is RISES ABOVE
|
||||
// If position is SHORT
|
||||
// TP is LONG and trigger is FALLS BELOW
|
||||
const takeProfitTriggerDirection =
|
||||
formValues.side === Schema.Side.SIDE_BUY
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW;
|
||||
// For direction it needs to be implied
|
||||
// If position is LONG (BUY)
|
||||
// SL is SHORT and trigger is FALLS BELOW
|
||||
// If position is SHORT
|
||||
// SL is LONG and trigger is RISES ABOVE
|
||||
const stopLossTriggerDirection =
|
||||
formValues.side === Schema.Side.SIDE_BUY
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE;
|
||||
|
||||
const stopOrdersSubmission = [];
|
||||
|
||||
// if there are both take profit and stop loss then the stop order needs to be OCO
|
||||
if (formValues.takeProfit && formValues.stopLoss) {
|
||||
const ocoStopOrderSubmission = mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
triggerPrice: formValues.stopLoss,
|
||||
ocoTriggerPrice: formValues.takeProfit,
|
||||
price: formValues.stopLoss,
|
||||
triggerDirection: stopLossTriggerDirection,
|
||||
triggerType: 'price',
|
||||
side: oppositeSide,
|
||||
expire: false,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
oco: true,
|
||||
ocoPrice: formValues.takeProfit,
|
||||
ocoTriggerType: 'price',
|
||||
ocoType: Schema.OrderType.TYPE_MARKET,
|
||||
ocoSize: formValues.size,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
stopOrdersSubmission.push(ocoStopOrderSubmission);
|
||||
} else if (formValues.takeProfit) {
|
||||
const takeProfitStopOrderSubmission = mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
price: formValues.takeProfit,
|
||||
triggerDirection: takeProfitTriggerDirection,
|
||||
triggerType: 'price',
|
||||
triggerPrice: formValues.takeProfit,
|
||||
side: oppositeSide,
|
||||
expire: false,
|
||||
ocoTriggerType: 'price',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
oco: false,
|
||||
ocoType: Schema.OrderType.TYPE_MARKET,
|
||||
ocoSize: formValues.size,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
stopOrdersSubmission.push(takeProfitStopOrderSubmission);
|
||||
} else if (formValues.stopLoss) {
|
||||
const stopLossStopOrderSubmission = mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
triggerPrice: formValues.stopLoss,
|
||||
price: formValues.stopLoss,
|
||||
triggerDirection: stopLossTriggerDirection,
|
||||
triggerType: 'price',
|
||||
side: oppositeSide,
|
||||
expire: false,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
oco: false,
|
||||
ocoTriggerType: 'price',
|
||||
ocoType: Schema.OrderType.TYPE_MARKET,
|
||||
ocoSize: formValues.size,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
stopOrdersSubmission.push(stopLossStopOrderSubmission);
|
||||
}
|
||||
|
||||
const batchMarketInstructions = {
|
||||
submissions: [orderSubmission],
|
||||
stopOrdersSubmission,
|
||||
};
|
||||
return batchMarketInstructions;
|
||||
};
|
||||
|
@ -1,8 +1,15 @@
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
|
||||
import type {
|
||||
OrderSubmissionBody,
|
||||
StopOrdersSubmission,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
mapFormValuesToOrderSubmission,
|
||||
mapFormValuesToTakeProfitAndStopLoss,
|
||||
} from './map-form-values-to-submission';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import type { OrderFormValues } from '../hooks';
|
||||
import { type MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||
|
||||
describe('mapFormValuesToOrderSubmission', () => {
|
||||
it('sets and formats price only for limit orders', () => {
|
||||
@ -186,3 +193,232 @@ describe('mapFormValuesToOrderSubmission', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const mockMarket: MarketFieldsFragment = {
|
||||
__typename: 'Market',
|
||||
id: 'marketId',
|
||||
decimalPlaces: 1,
|
||||
positionDecimalPlaces: 4,
|
||||
state: Schema.MarketState.STATE_ACTIVE,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
} as MarketFieldsFragment;
|
||||
|
||||
const orderFormValues: OrderFormValues = {
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '1',
|
||||
price: '66300',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
tpSl: true,
|
||||
takeProfit: '70000',
|
||||
stopLoss: '60000',
|
||||
};
|
||||
|
||||
describe('mapFormValuesToTakeProfitAndStopLoss', () => {
|
||||
it('creates batch market instructions for a normal order created with TP and SL', () => {
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValues,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [
|
||||
{
|
||||
fallsBelow: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '600000',
|
||||
},
|
||||
risesAbove: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '700000',
|
||||
},
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates batch market instructions for a normal order created without TP and SL', () => {
|
||||
// Create order form values without TP and SL
|
||||
const orderFormValuesWithoutTPSL = { ...orderFormValues };
|
||||
delete orderFormValuesWithoutTPSL.takeProfit;
|
||||
delete orderFormValuesWithoutTPSL.stopLoss;
|
||||
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValuesWithoutTPSL,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
// Expected result when TP and SL are not provided
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates batch market instructions for a normal order created with TP only', () => {
|
||||
// Create order form values with TP only
|
||||
const orderFormValuesWithTP = { ...orderFormValues };
|
||||
orderFormValuesWithTP.stopLoss = undefined;
|
||||
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValuesWithTP,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
// Expected result when only TP is provided
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [
|
||||
{
|
||||
risesAbove: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '700000',
|
||||
},
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates batch market instructions for a normal order created with SL only', () => {
|
||||
// Create order form values with SL only
|
||||
const orderFormValuesWithSL = { ...orderFormValues };
|
||||
orderFormValuesWithSL.takeProfit = undefined;
|
||||
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValuesWithSL,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
// Expected result when only SL is provided
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [
|
||||
{
|
||||
fallsBelow: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '600000',
|
||||
},
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
@ -66,7 +66,7 @@
|
||||
"Notional": "Notional",
|
||||
"NOTIONAL_SIZE_TOOLTIP_TEXT": "The notional size represents the position size in the settlement asset {{quoteName}} of the futures contract. This is calculated by multiplying the number of contracts by the prices of the contract. For example 10 contracts traded at a price of $50 has a notional size of $500.",
|
||||
"OCO": "OCO",
|
||||
"One cancels another": "One cancels another",
|
||||
"One cancels the other": "One cancels the other",
|
||||
"Only limit orders are permitted when market is in auction": "Only limit orders are permitted when market is in auction",
|
||||
"Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.": "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.",
|
||||
"You have an existing position on this market.": "You have an existing position on this market.",
|
||||
@ -135,6 +135,12 @@
|
||||
"Total margin available": "Total margin available",
|
||||
"TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).",
|
||||
"No trading": "No trading",
|
||||
"TP / SL": "TP / SL",
|
||||
"TP_SL_TOOLTIP": "Take profit / Stop loss",
|
||||
"Take profit": "Take profit",
|
||||
"Stop loss": "Stop loss",
|
||||
"The price for take profit.": "The price for take profit.",
|
||||
"The price for stop loss.": "The price for stop loss.",
|
||||
"Trailing percent offset cannot be higher than 99.9": "Trailing percent offset cannot be higher than 99.9",
|
||||
"Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}": "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}",
|
||||
"Trailing percentage offset": "Trailing percentage offset",
|
||||
|
@ -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