feat(deal-ticket): show maximum number of active stop orders limit warning (#4687)
Co-authored-by: Maciek <maciek@vegaprotocol.io> Co-authored-by: Edd <edd@vega.xyz> Co-authored-by: Joe Tsang <30622993+jtsang586@users.noreply.github.com> Co-authored-by: Sam Keen <samuel.kleinmann@gmail.com> Co-authored-by: Matthew Russell <mattrussell36@gmail.com> Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com> Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
This commit is contained in:
parent
c540d4b17f
commit
47a84b4dac
@ -39,7 +39,11 @@ export const MarketsPage = () => {
|
|||||||
id="proposed-markets"
|
id="proposed-markets"
|
||||||
name={t('Proposed markets')}
|
name={t('Proposed markets')}
|
||||||
menu={
|
menu={
|
||||||
<TradingAnchorButton size="extra-small" href={externalLink}>
|
<TradingAnchorButton
|
||||||
|
size="extra-small"
|
||||||
|
data-testid="propose-new-market"
|
||||||
|
href={externalLink}
|
||||||
|
>
|
||||||
{t('Propose a new market')}
|
{t('Propose a new market')}
|
||||||
</TradingAnchorButton>
|
</TradingAnchorButton>
|
||||||
}
|
}
|
||||||
|
@ -78,8 +78,18 @@ 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';
|
||||||
|
|
||||||
|
const numberOfActiveOrdersLimit = 'stop-order-warning-limit';
|
||||||
|
|
||||||
const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id);
|
const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id);
|
||||||
|
|
||||||
|
const mockDataProvider = jest.fn((...args) => ({
|
||||||
|
data: Array(0),
|
||||||
|
}));
|
||||||
|
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||||
|
useDataProvider: jest.fn((...args) => mockDataProvider(...args)),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('StopOrder', () => {
|
describe('StopOrder', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@ -460,4 +470,26 @@ describe('StopOrder', () => {
|
|||||||
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
|
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
|
||||||
).toEqual(now);
|
).toEqual(now);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows limit of active stop orders number', async () => {
|
||||||
|
mockDataProvider.mockReturnValue({
|
||||||
|
data: Array(4),
|
||||||
|
});
|
||||||
|
render(generateJsx());
|
||||||
|
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(true);
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
|
||||||
|
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(false);
|
||||||
|
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts oco as two orders', async () => {
|
||||||
|
mockDataProvider.mockReturnValue({
|
||||||
|
data: Array(3),
|
||||||
|
});
|
||||||
|
render(generateJsx());
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
|
||||||
|
expect(screen.queryByTestId(numberOfActiveOrdersLimit)).toBeNull();
|
||||||
|
await userEvent.click(screen.getByTestId(oco));
|
||||||
|
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
TradingButton as Button,
|
TradingButton as Button,
|
||||||
Pill,
|
Pill,
|
||||||
Intent,
|
Intent,
|
||||||
|
Notification,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { getDerivedPrice } from '@vegaprotocol/markets';
|
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||||
import type { Market } from '@vegaprotocol/markets';
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
@ -53,6 +54,8 @@ import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
|||||||
import { validateExpiration } from '../../utils';
|
import { validateExpiration } from '../../utils';
|
||||||
import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
|
import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
|
||||||
import { KeyValue } from './key-value';
|
import { KeyValue } from './key-value';
|
||||||
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
|
import { stopOrdersProvider } from '@vegaprotocol/orders';
|
||||||
|
|
||||||
export interface StopOrderProps {
|
export interface StopOrderProps {
|
||||||
market: Market;
|
market: Market;
|
||||||
@ -60,6 +63,8 @@ export interface StopOrderProps {
|
|||||||
submit: (order: StopOrdersSubmission) => void;
|
submit: (order: StopOrdersSubmission) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_NUMBER_OF_ACTIVE_STOP_ORDERS = 4;
|
||||||
|
const POLLING_TIME = 2000;
|
||||||
const trailingPercentOffsetStep = '0.1';
|
const trailingPercentOffsetStep = '0.1';
|
||||||
|
|
||||||
const getDefaultValues = (
|
const getDefaultValues = (
|
||||||
@ -802,6 +807,27 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
const triggerTrailingPercentOffset = watch('triggerTrailingPercentOffset');
|
const triggerTrailingPercentOffset = watch('triggerTrailingPercentOffset');
|
||||||
const triggerType = watch('triggerType');
|
const triggerType = watch('triggerType');
|
||||||
|
|
||||||
|
const { data: activeStopOrders, reload } = useDataProvider({
|
||||||
|
dataProvider: stopOrdersProvider,
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
parties: pubKey ? [pubKey] : [],
|
||||||
|
markets: [market.id],
|
||||||
|
liveOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skip: !(pubKey && (formState.isDirty || formState.submitCount)),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
reload();
|
||||||
|
}, POLLING_TIME);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedSize = storedFormValues?.[dealTicketType]?.size;
|
const storedSize = storedFormValues?.[dealTicketType]?.size;
|
||||||
if (storedSize && size !== storedSize) {
|
if (storedSize && size !== storedSize) {
|
||||||
@ -864,7 +890,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
{errors.type.message}
|
{errors.type.message}
|
||||||
</InputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="side"
|
name="side"
|
||||||
control={control}
|
control={control}
|
||||||
@ -1095,6 +1120,20 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<NoWalletWarning isReadOnly={isReadOnly} />
|
<NoWalletWarning isReadOnly={isReadOnly} />
|
||||||
|
{(activeStopOrders?.length ?? 0) + (oco ? 2 : 1) >
|
||||||
|
MAX_NUMBER_OF_ACTIVE_STOP_ORDERS ? (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Warning}
|
||||||
|
testId={'stop-order-warning-limit'}
|
||||||
|
message={t(
|
||||||
|
'There is a limit of %s active stop orders per market. Orders submitted above the limit will be immediately rejected.',
|
||||||
|
[MAX_NUMBER_OF_ACTIVE_STOP_ORDERS.toString()]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
assetUnit={assetUnit}
|
assetUnit={assetUnit}
|
||||||
market={market}
|
market={market}
|
||||||
|
@ -144,8 +144,8 @@ fragment StopOrderFields on StopOrder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query StopOrders($partyId: ID!) {
|
query StopOrders($filter: StopOrderFilter) {
|
||||||
stopOrders(filter: { parties: [$partyId] }) {
|
stopOrders(filter: $filter) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
...StopOrderFields
|
...StopOrderFields
|
||||||
|
@ -37,7 +37,7 @@ export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', ma
|
|||||||
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, order?: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null } | null, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
|
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, order?: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null } | null, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
|
||||||
|
|
||||||
export type StopOrdersQueryVariables = Types.Exact<{
|
export type StopOrdersQueryVariables = Types.Exact<{
|
||||||
partyId: Types.Scalars['ID'];
|
filter?: Types.InputMaybe<Types.StopOrderFilter>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
@ -283,8 +283,8 @@ export function useOrdersUpdateSubscription(baseOptions: Apollo.SubscriptionHook
|
|||||||
export type OrdersUpdateSubscriptionHookResult = ReturnType<typeof useOrdersUpdateSubscription>;
|
export type OrdersUpdateSubscriptionHookResult = ReturnType<typeof useOrdersUpdateSubscription>;
|
||||||
export type OrdersUpdateSubscriptionResult = Apollo.SubscriptionResult<OrdersUpdateSubscription>;
|
export type OrdersUpdateSubscriptionResult = Apollo.SubscriptionResult<OrdersUpdateSubscription>;
|
||||||
export const StopOrdersDocument = gql`
|
export const StopOrdersDocument = gql`
|
||||||
query StopOrders($partyId: ID!) {
|
query StopOrders($filter: StopOrderFilter) {
|
||||||
stopOrders(filter: {parties: [$partyId]}) {
|
stopOrders(filter: $filter) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
...StopOrderFields
|
...StopOrderFields
|
||||||
@ -306,11 +306,11 @@ export const StopOrdersDocument = gql`
|
|||||||
* @example
|
* @example
|
||||||
* const { data, loading, error } = useStopOrdersQuery({
|
* const { data, loading, error } = useStopOrdersQuery({
|
||||||
* variables: {
|
* variables: {
|
||||||
* partyId: // value for 'partyId'
|
* filter: // value for 'filter'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function useStopOrdersQuery(baseOptions: Apollo.QueryHookOptions<StopOrdersQuery, StopOrdersQueryVariables>) {
|
export function useStopOrdersQuery(baseOptions?: Apollo.QueryHookOptions<StopOrdersQuery, StopOrdersQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useQuery<StopOrdersQuery, StopOrdersQueryVariables>(StopOrdersDocument, options);
|
return Apollo.useQuery<StopOrdersQuery, StopOrdersQueryVariables>(StopOrdersDocument, options);
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './__generated__/Orders';
|
export * from './__generated__/Orders';
|
||||||
export * from './order-data-provider';
|
export * from './order-data-provider';
|
||||||
|
export * from './stop-orders-data-provider';
|
||||||
|
@ -7,7 +7,7 @@ import type { StopOrder } from '../order-data-provider/stop-orders-data-provider
|
|||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import { stopOrdersWithMarketProvider } from '../order-data-provider/stop-orders-data-provider';
|
import { stopOrdersWithMarketProvider } from '../order-data-provider/stop-orders-data-provider';
|
||||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||||
import type { Order } from '../order-data-provider';
|
import type { Order, StopOrdersQueryVariables } from '../order-data-provider';
|
||||||
|
|
||||||
export interface StopOrdersManagerProps {
|
export interface StopOrdersManagerProps {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
@ -26,7 +26,11 @@ export const StopOrdersManager = ({
|
|||||||
}: StopOrdersManagerProps) => {
|
}: StopOrdersManagerProps) => {
|
||||||
const create = useVegaTransactionStore((state) => state.create);
|
const create = useVegaTransactionStore((state) => state.create);
|
||||||
const [viewOrder, setViewOrder] = useState<Order | null>(null);
|
const [viewOrder, setViewOrder] = useState<Order | null>(null);
|
||||||
const variables = { partyId };
|
const variables: StopOrdersQueryVariables = {
|
||||||
|
filter: {
|
||||||
|
parties: [partyId],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const { data, error, reload } = useDataProvider({
|
const { data, error, reload } = useDataProvider({
|
||||||
dataProvider: stopOrdersWithMarketProvider,
|
dataProvider: stopOrdersWithMarketProvider,
|
||||||
|
@ -132,6 +132,7 @@ export const TradingAnchorButton = forwardRef<
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
subLabel,
|
subLabel,
|
||||||
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => (
|
) => (
|
||||||
@ -139,6 +140,7 @@ export const TradingAnchorButton = forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
href={href}
|
href={href}
|
||||||
className={getClassName({ size, subLabel, intent }, className)}
|
className={getClassName({ size, subLabel, intent }, className)}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<Content icon={icon} subLabel={subLabel} children={children} />
|
<Content icon={icon} subLabel={subLabel} children={children} />
|
||||||
</a>
|
</a>
|
||||||
|
Loading…
Reference in New Issue
Block a user