Skip to content

Commit 6d2a205

Browse files
feat(perps): show a trading halt notice when the deviation is above threshold (MetaMask#23242)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR adds a price deviation warning feature to the Perps trading interface. When the perps price deviates more than 10% from the spot price (mark price), a warning banner is displayed to inform users that new positions cannot be opened at that time. This helps protect users from executing trades at unfavorable prices during periods of significant price divergence. ## **Changelog** CHANGELOG entry: Added price deviation warning to Perps trading interface to prevent opening positions when perps price deviates significantly from spot price ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2047 ## **Manual testing steps** ```gherkin Feature: Perps Price Deviation Warning Scenario: user views market details when price deviation is within threshold Given user is on the Perps Market Details view for BTC And the asset price is within 10% of the spot price When user views the market details page Then no price deviation warning should be displayed Scenario: user views market details when price deviation exceeds threshold Given user is on the Perps Market Details view for BTC And the perps price deviates more than 10% from the spot price When user views the market details page Then a price deviation warning banner should be displayed And the warning message should indicate that new positions cannot be opened Scenario: warning updates dynamically as price changes Given user is on the Perps Market Details view for BTC And the perps price initially deviates more than 10% from spot price When the price deviation decreases below 10% Then the warning banner should disappear ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** No notice shown ### **After** Notice shown <img width="1170" height="2532" alt="Simulator Screenshot - iPhone 16e - 2025-11-25 at 11 17 03" src="https://github.com/user-attachments/assets/02cfb5d5-f57f-4101-8e8c-a852af73b606" /> ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a price deviation check and warning banner that disables trade actions when perps price deviates beyond a configured threshold from spot. > > - **UI** > - Add `PerpsPriceDeviationWarning` component and render it in `PerpsMarketDetailsView` when `useIsPriceDeviatedAboveThreshold` signals trading halt. > - Hide fixed actions footer (Add Funds / Long / Short / Modify / Close) while trading is halted. > - Minor cleanup: remove unnecessary fragment around `TradingViewChart`. > - **Hooks** > - New `useIsPriceDeviatedAboveThreshold(symbol)` leveraging `usePerpsPrices` to detect deviation; exported via `hooks/index`. > - **Config** > - Add `VALIDATION_THRESHOLDS.PRICE_DEVIATION` (10%). > - **i18n** > - Add `perps.price_deviation_warning.message` copy. > - **Tests** > - Add comprehensive unit tests for `useIsPriceDeviatedAboveThreshold`. > - Update `PerpsMarketDetailsView.test.tsx` with mocks for new hook and connections; ensure rendering compatibility. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 81d14af. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent b966482 commit 6d2a205

11 files changed

Lines changed: 590 additions & 16 deletions

File tree

app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,32 @@ jest.mock('react-redux', () => ({
182182
useSelector: jest.fn(),
183183
}));
184184

185+
// Mock usePerpsConnection hook directly to ensure all hooks that import it get the mock
186+
jest.mock('../../hooks/usePerpsConnection', () => ({
187+
usePerpsConnection: () => ({
188+
isConnected: true,
189+
isConnecting: false,
190+
isInitialized: true,
191+
error: null,
192+
connect: jest.fn(),
193+
disconnect: jest.fn(),
194+
resetError: jest.fn(),
195+
reconnectWithNewContext: jest.fn(),
196+
}),
197+
}));
198+
185199
jest.mock('../../providers/PerpsConnectionProvider', () => ({
186200
PerpsConnectionProvider: ({ children }: { children: React.ReactNode }) =>
187201
children,
188202
usePerpsConnection: () => ({
189203
isConnected: true,
190204
isConnecting: false,
205+
isInitialized: true,
191206
error: null,
207+
connect: jest.fn(),
208+
disconnect: jest.fn(),
209+
resetError: jest.fn(),
210+
reconnectWithNewContext: jest.fn(),
192211
}),
193212
}));
194213

@@ -266,12 +285,28 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({
266285
})),
267286
}));
268287

288+
jest.mock('../../hooks/usePerpsPrices', () => ({
289+
usePerpsPrices: jest.fn(() => ({})),
290+
}));
291+
292+
jest.mock('../../hooks/useIsPriceDeviatedAboveThreshold', () => ({
293+
useIsPriceDeviatedAboveThreshold: jest.fn(() => ({
294+
isDeviatedAboveThreshold: false,
295+
isLoading: false,
296+
})),
297+
}));
298+
269299
jest.mock('../../hooks', () => ({
270300
usePerpsLiveAccount: () => mockUsePerpsAccount(),
271301
usePerpsConnection: () => ({
272302
isConnected: true,
273303
isConnecting: false,
304+
isInitialized: true,
274305
error: null,
306+
connect: jest.fn(),
307+
disconnect: jest.fn(),
308+
resetError: jest.fn(),
309+
reconnectWithNewContext: jest.fn(),
275310
}),
276311
usePerpsOpenOrders: () => ({
277312
orders: [],

app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
usePerpsDataMonitor,
6969
type DataMonitorParams,
7070
} from '../../hooks/usePerpsDataMonitor';
71+
import { useIsPriceDeviatedAboveThreshold } from '../../hooks/useIsPriceDeviatedAboveThreshold';
7172
import { usePerpsMeasurement } from '../../hooks/usePerpsMeasurement';
7273
import {
7374
usePerpsLiveAccount,
@@ -81,6 +82,7 @@ import PerpsPositionCard from '../../components/PerpsPositionCard';
8182
import PerpsMarketStatisticsCard from '../../components/PerpsMarketStatisticsCard';
8283
import type { PerpsTooltipContentKey } from '../../components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.types';
8384
import PerpsOICapWarning from '../../components/PerpsOICapWarning';
85+
import PerpsPriceDeviationWarning from '../../components/PerpsPriceDeviationWarning';
8486
import PerpsNotificationTooltip from '../../components/PerpsNotificationTooltip';
8587
import PerpsNavigationCard, {
8688
type NavigationItem,
@@ -295,6 +297,12 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
295297
// Check if market is at open interest cap
296298
const { isAtCap: isAtOICap } = usePerpsOICap(market?.symbol);
297299

300+
// Check if trading is halted due to price deviation
301+
const {
302+
isDeviatedAboveThreshold: isTradingHalted,
303+
isLoading: isLoadingTradingHalted,
304+
} = useIsPriceDeviatedAboveThreshold(market?.symbol);
305+
298306
// Handle data-driven monitoring when coming from order success
299307
// Clear monitoringIntent after processing to allow fresh monitoring next time
300308
const handleDataDetected = useCallback(() => {
@@ -894,21 +902,19 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
894902
)}
895903

896904
{hasHistoricalData ? (
897-
<>
898-
<TradingViewChart
899-
ref={chartRef}
900-
candleData={candleData}
901-
height={PERPS_CHART_CONFIG.LAYOUT.DETAIL_VIEW_HEIGHT}
902-
visibleCandleCount={visibleCandleCount}
903-
tpslLines={tpslLines}
904-
symbol={market?.symbol}
905-
showOverlay={false}
906-
coloredVolume
907-
onOhlcDataChange={setOhlcData}
908-
onNeedMoreHistory={fetchMoreHistory}
909-
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-tradingview-chart`}
910-
/>
911-
</>
905+
<TradingViewChart
906+
ref={chartRef}
907+
candleData={candleData}
908+
height={PERPS_CHART_CONFIG.LAYOUT.DETAIL_VIEW_HEIGHT}
909+
visibleCandleCount={visibleCandleCount}
910+
tpslLines={tpslLines}
911+
symbol={market?.symbol}
912+
showOverlay={false}
913+
coloredVolume
914+
onOhlcDataChange={setOhlcData}
915+
onNeedMoreHistory={fetchMoreHistory}
916+
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-tradingview-chart`}
917+
/>
912918
) : (
913919
<Skeleton
914920
height={PERPS_CHART_CONFIG.LAYOUT.DETAIL_VIEW_HEIGHT}
@@ -925,6 +931,13 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
925931
onMorePress={handleMorePress}
926932
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-candle-period-selector`}
927933
/>
934+
935+
{/* Price Deviation Warning - Shows when price has deviated too much from spot price */}
936+
{market?.symbol && isTradingHalted && !isLoadingTradingHalted && (
937+
<PerpsPriceDeviationWarning
938+
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-price-deviation-warning`}
939+
/>
940+
)}
928941
</View>
929942

930943
{/* OI Cap Warning - Shows when market is at capacity */}
@@ -1035,7 +1048,7 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
10351048
</View>
10361049

10371050
{/* Fixed Actions Footer */}
1038-
{(hasAddFundsButton || hasLongShortButtons) && (
1051+
{(hasAddFundsButton || hasLongShortButtons) && !isTradingHalted && (
10391052
<View style={styles.actionsFooter}>
10401053
{hasAddFundsButton && (
10411054
<View style={styles.singleActionContainer}>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { StyleSheet } from 'react-native';
2+
import type { Theme } from '../../../../../util/theme/models';
3+
4+
const styleSheet = (params: { theme: Theme }) => {
5+
const { colors } = params.theme;
6+
7+
return StyleSheet.create({
8+
container: {
9+
flexDirection: 'row',
10+
alignItems: 'center',
11+
gap: 8,
12+
backgroundColor: colors.background.muted,
13+
borderRadius: 12,
14+
padding: 12,
15+
marginTop: 8,
16+
},
17+
icon: {},
18+
textContainer: {
19+
flex: 1,
20+
gap: 4,
21+
},
22+
});
23+
};
24+
25+
export default styleSheet;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { memo } from 'react';
2+
import { View } from 'react-native';
3+
import { useStyles } from '../../../../../component-library/hooks';
4+
import Text, {
5+
TextVariant,
6+
TextColor,
7+
} from '../../../../../component-library/components/Texts/Text';
8+
import Icon, {
9+
IconName,
10+
IconSize,
11+
IconColor,
12+
} from '../../../../../component-library/components/Icons/Icon';
13+
import { strings } from '../../../../../../locales/i18n';
14+
import type { PerpsPriceDeviationWarningProps } from './PerpsPriceDeviationWarning.types';
15+
import styleSheet from './PerpsPriceDeviationWarning.styles';
16+
17+
/**
18+
* Component that displays a warning when the perps price has deviated too much from the spot price
19+
* This prevents users from opening new positions when the price is significantly different from the spot price
20+
*
21+
* **Performance:**
22+
* - Memoized to prevent unnecessary re-renders
23+
*
24+
* @example
25+
* ```tsx
26+
* <PerpsPriceDeviationWarning />
27+
* ```
28+
*/
29+
const PerpsPriceDeviationWarning: React.FC<PerpsPriceDeviationWarningProps> =
30+
memo(({ testID = 'perps-price-deviation-warning' }) => {
31+
const { styles } = useStyles(styleSheet, {});
32+
33+
return (
34+
<View style={styles.container} testID={testID}>
35+
<Icon
36+
name={IconName.Info}
37+
size={IconSize.Md}
38+
color={IconColor.Default}
39+
style={styles.icon}
40+
/>
41+
<View style={styles.textContainer}>
42+
<Text variant={TextVariant.BodyMD} color={TextColor.Default}>
43+
{strings('perps.price_deviation_warning.message')}
44+
</Text>
45+
</View>
46+
</View>
47+
);
48+
});
49+
50+
PerpsPriceDeviationWarning.displayName = 'PerpsPriceDeviationWarning';
51+
52+
export default PerpsPriceDeviationWarning;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface PerpsPriceDeviationWarningProps {
2+
/** Optional test ID for testing */
3+
testID?: string;
4+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './PerpsPriceDeviationWarning';
2+
export type { PerpsPriceDeviationWarningProps } from './PerpsPriceDeviationWarning.types';

app/components/UI/Perps/constants/perpsConfig.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export const VALIDATION_THRESHOLDS = {
7171

7272
// Limit price difference threshold (as decimal, 0.1 = 10%)
7373
LIMIT_PRICE_DIFFERENCE_WARNING: 0.1, // Warn if limit price differs by >10% from current price
74+
75+
// Price deviation threshold (as decimal, 0.1 = 10%)
76+
PRICE_DEVIATION: 0.1, // Warn if perps price deviates by >10% from spot price
7477
} as const;
7578

7679
/**

app/components/UI/Perps/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export { usePerpsOrderForm } from './usePerpsOrderForm';
6363
export { usePerpsOrderValidation } from './usePerpsOrderValidation';
6464
export { usePerpsClosePositionValidation } from './usePerpsClosePositionValidation';
6565
export { usePerpsOrderExecution } from './usePerpsOrderExecution';
66+
export { useIsPriceDeviatedAboveThreshold } from './useIsPriceDeviatedAboveThreshold';
6667
export { usePerpsFirstTimeUser } from './usePerpsFirstTimeUser';
6768
export { usePerpsTPSLForm } from './usePerpsTPSLForm';
6869
export { default as usePerpsToasts } from './usePerpsToasts';

0 commit comments

Comments
 (0)