Skip to content

Commit d52e9e9

Browse files
chore(runway): cherry-pick fix: compute spread from HL bbo top-of-book feed cp-7.63.0 (#25164)
- fix: compute spread from HL bbo top-of-book feed cp-7.63.0 (#25145) <!-- 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** ### Summary - Fixes incorrect spread display in Perps order book that changed when users change the aggregate dropdown - Spread now uses BBO (best bid/offer) feed instead of aggregated L2Book, matching Hyperliquid UI behavior - Adds missing test coverage for `subscribeToOrderBook` L2Book subscriptions https://consensyssoftware.atlassian.net/browse/TAT-2425 ### Problem The spread displayed under the order book depth chart was incorrect and would change when the use changed the aggregation dropdown. This diverged from Hyperliquid's UI where spread remains stable regardless of grouping selection. Root cause: Spread was derived from aggregated orderbook levels. When requesting an aggregated book, the best bid/ask are bucketed/rounded prices, causing the spread to inflate to increments resembling the grouping step. ### Solution Split the data sources: - Depth/table display: `subscribeToOrderBook` -> `l2Book` with aggregation params (existing) - Spread display: `subscribeToPrices(includeOrderBook: true)` -> bbo feed (new) The L2Book -> BBO change only affects `subscribeToPrices({ includeOrderBook: true })`, which feeds `usePerpsTopOfBook`. All 5 consumers only need best bid/ask: - Fee calculation (3 views) - compares limit price to best bid/ask for maker/taker determination - Bid/Ask presets - single best prices for quick buttons - Spread display - bestAsk - bestBid Full L2Book depth is still used via separate `subscribeToOrderBook()` path for the order book table/chart. This matches Hyperliquid's frontend: grouping affects the book display, spread is based on actual top-of-book. ### Test plan - Unit tests for processBboData - Unit tests for BBO subscription lifecycle - Unit tests for subscribeToOrderBook (L2Book) - 10 new tests <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? AI agent: Be specific about what you changed and why. Include context about the fix/feature, not generic descriptions. --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) AI agent: Use format `CHANGELOG entry: [fix/feat/chore]: [User-facing description in past tense]`. Examples: `fix: resolved token name display issue`, `feat: added dark mode toggle`, `chore: updated dependencies`. For non-user-facing changes, use `CHANGELOG entry: null`. --> CHANGELOG entry: Fixed incorrect spread displayed below Perps orderbook depth chart ## **Related issues** <!-- AI agent: Replace with `Fixes: #[ISSUE_NUMBER]` using the actual issue number you're implementing. --> Fixes: #25162 ## **Manual testing steps** - Verify spread matches Hyperliquid UI across all grouping values - Verify changing grouping doesn't affect spread display <!-- AI agent: Write specific, contextual Gherkin steps based on what you actually implemented. Do NOT use generic placeholders like "my feature name". Be concrete about the feature, scenario, and steps. --> ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/43c6c40c-162c-4809-9295-1acaf684d64d ## **Pre-merge author checklist** <!-- AI agent: Check ALL boxes in this section (mark all as [x]). --> - [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** <!-- AI agent: Leave ALL boxes unchecked ([ ]) - these are for reviewers to check, not the author. --> - [ ] 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] > **Fix spread to match Hyperliquid UI** > > - Switch spread display in `PerpsOrderBookView` to `usePerpsTopOfBook` (BBO), independent of order book grouping; format via `formatPerpsFiat` > - Update `HyperLiquidSubscriptionService`: replace `l2Book`-based top-of-book with singleton `bbo` subscriptions (`ensureBboSubscription`/`cleanupBboSubscription`), reconnection restore logic, and cache updates via new `processBboData` > - Keep full depth via existing `subscribeToOrderBook` (L2Book); add comprehensive tests for this path plus new BBO lifecycle and processor tests > - Adjust tests to mock `usePerpsTopOfBook` and verify spread rendering and subscription params > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 33d5529. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [6ca25dd](6ca25dd) Co-authored-by: Matt D. <85914066+geositta@users.noreply.github.com>
1 parent 526aa42 commit d52e9e9

6 files changed

Lines changed: 755 additions & 72 deletions

File tree

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,17 @@ jest.mock('../../hooks/usePerpsOrderBookGrouping', () => ({
175175
})),
176176
}));
177177

178+
// Mock usePerpsTopOfBook
179+
const mockUsePerpsTopOfBook = jest.fn(() => ({
180+
bestBid: '50000',
181+
bestAsk: '50001',
182+
spread: '1.00000',
183+
}));
184+
185+
jest.mock('../../hooks/stream/usePerpsTopOfBook', () => ({
186+
usePerpsTopOfBook: () => mockUsePerpsTopOfBook(),
187+
}));
188+
178189
// Mock usePerpsEventTracking
179190
const mockTrack = jest.fn();
180191

@@ -285,6 +296,11 @@ describe('PerpsOrderBookView', () => {
285296
isLoading: false,
286297
error: null,
287298
});
299+
mockUsePerpsTopOfBook.mockReturnValue({
300+
bestBid: '50000',
301+
bestAsk: '50001',
302+
spread: '1.00000',
303+
});
288304
});
289305

290306
describe('rendering', () => {

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,17 @@ import {
6565
} from '../../hooks';
6666
import { useHasExistingPosition } from '../../hooks/useHasExistingPosition';
6767
import { usePerpsLiveOrderBook } from '../../hooks/stream/usePerpsLiveOrderBook';
68+
import { usePerpsTopOfBook } from '../../hooks/stream/usePerpsTopOfBook';
6869
import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking';
6970
import { usePerpsMeasurement } from '../../hooks/usePerpsMeasurement';
7071
import { usePerpsOrderBookGrouping } from '../../hooks/usePerpsOrderBookGrouping';
7172
import { selectPerpsButtonColorTestVariant } from '../../selectors/featureFlags';
7273
import { BUTTON_COLOR_TEST } from '../../utils/abTesting/tests';
7374
import { usePerpsABTest } from '../../utils/abTesting/usePerpsABTest';
75+
import {
76+
formatPerpsFiat,
77+
PRICE_RANGES_UNIVERSAL,
78+
} from '../../utils/formatUtils';
7479
import { getPerpsDisplaySymbol } from '../../utils/marketUtils';
7580
import {
7681
calculateAggregationParams,
@@ -187,6 +192,37 @@ const PerpsOrderBookView: React.FC<PerpsOrderBookViewProps> = ({
187192
return null;
188193
}, [selectedGrouping, groupingOptions]);
189194

195+
// Subscribe to top-of-book (best bid/ask) for spread display.
196+
// This is intentionally independent from order book aggregation/grouping.
197+
const topOfBook = usePerpsTopOfBook({ symbol: symbol || '' });
198+
199+
const spreadMetrics = useMemo(() => {
200+
const bidStr = topOfBook?.bestBid;
201+
const askStr = topOfBook?.bestAsk;
202+
if (!bidStr || !askStr) return null;
203+
204+
const bid = parseFloat(bidStr);
205+
const ask = parseFloat(askStr);
206+
if (
207+
!Number.isFinite(bid) ||
208+
!Number.isFinite(ask) ||
209+
bid <= 0 ||
210+
ask <= 0
211+
) {
212+
return null;
213+
}
214+
215+
// Round to eliminate floating point artifacts (e.g., 0.09999999999990905 → 0.1)
216+
const spread = Number((ask - bid).toPrecision(10));
217+
const mid = (ask + bid) / 2;
218+
const spreadPercentage = mid > 0 ? ((spread / mid) * 100).toFixed(3) : '0';
219+
220+
return {
221+
spread,
222+
spreadPercentage,
223+
};
224+
}, [topOfBook]);
225+
190226
// Calculate aggregation params (nSigFigs + mantissa) based on grouping
191227
const aggregationParams = useMemo(() => {
192228
if (!marketPrice || !currentGrouping) return { nSigFigs: 5 as const };
@@ -215,8 +251,6 @@ const PerpsOrderBookView: React.FC<PerpsOrderBookViewProps> = ({
215251
return rawOrderBook;
216252
}
217253

218-
// No client-side aggregation needed - API handles it via nSigFigs
219-
// Just return the raw order book data directly
220254
return rawOrderBook;
221255
}, [rawOrderBook]);
222256

@@ -506,16 +540,18 @@ const PerpsOrderBookView: React.FC<PerpsOrderBookViewProps> = ({
506540
{/* Footer with Spread and Actions */}
507541
<View style={footerStyle}>
508542
{/* Spread Row */}
509-
{orderBook && (
543+
{spreadMetrics && (
510544
<View style={styles.spreadContainer}>
511545
<Text variant={TextVariant.BodySM} color={TextColor.Alternative}>
512546
{strings('perps.order_book.spread')}:
513547
</Text>
514548
<Text variant={TextVariant.BodySM} color={TextColor.Default}>
515-
${parseFloat(orderBook.spread).toLocaleString()}
549+
{formatPerpsFiat(spreadMetrics.spread, {
550+
ranges: PRICE_RANGES_UNIVERSAL,
551+
})}
516552
</Text>
517553
<Text variant={TextVariant.BodySM} color={TextColor.Alternative}>
518-
({orderBook.spreadPercentage}%)
554+
({spreadMetrics.spreadPercentage}%)
519555
</Text>
520556
<TouchableOpacity
521557
onPress={() => handleTooltipPress('spread')}

0 commit comments

Comments
 (0)