feat(perps): add perps slippage controls#30125
Conversation
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
Store max slippage as basis points (integers) in PerpsController state. Show estimated slippage from order book VWAP on market orders, with configurable max slippage via bottom sheet. Block orders that exceed the user's max slippage setting.
Tests cover bps/pct conversion, display formatting edge cases, and order book VWAP slippage computation for both long/short directions including insufficient liquidity and invalid data handling.
- Replace magic number 100 with ORDER_SLIPPAGE_CONFIG.DefaultLimitSlippageBps - Fix MetaMetrics MAX_SLIPPAGE_PCT to send percent instead of raw BPS - Centralize 4 inline testIDs into PerpsSlippageConfigSelectorsIDs - Remove unused SLIPPAGE_LIMIT_BLOCKED_ORDER event value - Add missing OrderBookData/OrderBookLevel fields in test helper
- Add MAX_SLIPPAGE_BOUNDS constant to perpsConfig.ts (MinBps/MaxBps/StepBps) - Use MAX_SLIPPAGE_BOUNDS in setMaxSlippage() instead of magic numbers - Derive UI-side slippageConfig.ts bounds from shared constant - Remove unused MAX_SLIPPAGE_SOURCE and ESTIMATED_SLIPPAGE_PCT event properties
Worker reportTAT-1043: Slippage Visualization and Configuration — ReportSummaryAdded slippage visualization and configuration to the perps market order screen. Users see estimated slippage computed from live order book depth (VWAP), can configure max slippage (0.1%-10%, default 3%) via a bottom sheet, and orders exceeding the configured max are blocked client-side. Changes
Test Plan
Evidence
Self-Review Fixes
Self-Review Fixes (Round 2)
Ticket |
… (TAT-1043) When the order book has insufficient liquidity, estimatedSlippagePct is null and exceedsMaxSlippage evaluated to false, allowing orders through. Now also blocks when insufficientLiquidity is true.
…ded (TAT-1043) When insufficientLiquidity is true, estimatedSlippagePct is null so the error previously showed "0%" which is misleading. Now shows "Insufficient liquidity to fill this order within your max slippage".
…ps-slippage-controls
- Empty order book side now correctly returns insufficientLiquidity: true instead of EMPTY_RESULT (which had insufficientLiquidity: false) - Removed custom memo comparator from PerpsSlippageBottomSheet that omitted onSave/onClose props, causing stale closure risk
Worker reportPR #30125 — Review Comments ReportSummary
Triage
|
…erage SonarCloud quality gate failed with 67.6% new code coverage (80% required). Add 9 tests covering render, quick picks, validation, save, and error states.
…ps-slippage-controls
Replace toBeTruthy() with toBeOnTheScreen() for element presence assertions per project unit testing guidelines.
Worker reportPR #30125 — Comments Triage ReportSummary
Triage
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #30125 +/- ##
===========================================
- Coverage 81.54% 44.62% -36.93%
===========================================
Files 5343 5397 +54
Lines 142128 143940 +1812
Branches 32411 32876 +465
===========================================
- Hits 115899 64233 -51666
- Misses 18299 73694 +55395
+ Partials 7930 6013 -1917 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ders
Implements the remaining Jira §4 in-scope items (AC1 and AC5) that the
prior commits hadn't shipped:
- Estimated slippage computed from the live HyperLiquid order book via
VWAP walk of the asks (BUY) / bids (SELL) up to the requested USD
notional. Result returned in basis points; null when the book has
insufficient depth.
- New `usePerpsEstimatedSlippage` hook wraps `usePerpsLiveOrderBook` so
the order screen can subscribe per asset, throttled at 250ms, and
only on market orders with a valid amount.
- Order screen slippage row renders the live estimate alongside the
configured max (i18n updated to `Est: {{est}}% / Max: {{value}}%`),
switching to error color when the estimate exceeds the cap.
- Submit handler blocks market orders whose estimated slippage exceeds
the user-configured cap, shows a toast, and fires a new
`slippage_limit_blocked_order` interaction event carrying both the
max and estimated percentages and the source (default vs
user_configured).
- Adds `max_slippage_source` and `estimated_slippage_pct` properties
plus a `MAX_SLIPPAGE_SOURCE` value block to the perps event constants
to match the Jira §7 instrumentation requirements.
…nstants Move the order-book level count and throttle window used by `usePerpsEstimatedSlippage` into `PERFORMANCE_CONFIG` (`SlippageEstimateBookLevels`, `SlippageEstimateThrottleMs`) so the tuning sits alongside the other perps timing constants and matches the anti-pattern guidance about avoiding inline magic numbers.
Round 1 cross-review from Codex and Claude flagged three actionable gaps on the new estimated-slippage path. Resolve all three: - Stop defaulting an unavailable estimate to `0` percent. When the L2 order book has not produced data yet (or is too shallow to fill the requested size), the slippage row now renders `Est: -- / Max: X%` via a new `row_format_pending` i18n template and `estimatedSlippagePct` is kept nullable. The user-configured cap still flows to HyperLiquid as the limit-price buffer, so we surface "pending" without blocking the order — but the misleading `Est: 0%` placeholder is gone. - Expose the displayed slippage value to agentic recipes by adding a dedicated `SLIPPAGE_VALUE` testID (`perps-order-view-slippage-value`) on the inner `<Text>` and registering it in `Perps.testIds.ts`. - Add `slippageCalculation.test.ts` covering the VWAP walk for null books, missing/zero levels, shallow depth, exact and partial fills, buy/sell direction, and the non-negative clamp. Also document why `usePerpsEstimatedSlippage` overrides the doc's 10s order-form WS guideline with a 250ms throttle — slippage needs sub- second updates while the user types, and the downstream `useMemo` keeps per-tick work cheap. Deferred nits: - Adding `exceedsMaxSlippage` to the place-order button's `isDisabled` would suppress the `slippage_limit_blocked_order` analytics event, which the product team needs for the orders-blocked-by-slippage guardrail. Keeping the toast-on-tap path preserves that signal. - Existing direct `PerpsController.get/setMaxSlippage` reads stay in the component to match the surrounding pattern in `PerpsOrderView`; moving them behind a hook is a broader refactor.
…eads Round 2 cross-review flagged two real issues that this commit fixes: - VWAP estimator walked the L2 book by quote notional, but the HyperLiquid provider executes a fixed base size derived from `usdValue / currentPrice`. The estimator now derives the same base target and walks levels by base size, so the bps figure matches the shape of the actual fill instead of underestimating buy slippage (and overestimating sell slippage). The unit tests now assert exact bps for both buy and sell two-level fills (`333.33` bps for the same `[100/110]` and `[100/90]` books at `$1500`), which is what caught the regression. - New `usePerpsMaxSlippage` hook owns the controller reads/writes so `PerpsOrderView` no longer calls `Engine.context.PerpsController` directly. The hook returns the resolved bps value, the source (`default` vs `user_configured`), and a setter that refreshes the memoised read after a save. This addresses the anti-pattern doc's "Direct controller call from component" rule for the slippage path.
…n tests Round 3 cross-review surfaced two real publisher/runtime gaps plus three test/constant nits. Resolve them in one pass: - `usePerpsEstimatedSlippage` now only subscribes once `usePerpsConnection().isInitialized` flips true. Without the gate the hook could subscribe before the perps provider was wired, `PerpsController.subscribeToOrderBook` would return a no-op, and the AC1 estimate plus AC5 block would stay disabled until the screen was remounted. - Re-add a deprecated `OrderParams.slippage` decimal alongside `maxSlippageBps` and normalize at the HyperLiquid provider boundary (`placeOrder` and `editOrder`). Removing the field outright in the prior refactor broke the publisher contract for any extension/core consumer still passing the decimal form; the shim lets us collapse to a single bps field internally while giving downstream consumers a deprecation window. - `usePerpsMaxSlippage` now defaults to `ORDER_SLIPPAGE_CONFIG.DefaultMarketSlippageBps` instead of the duplicate UI `PERPS_SLIPPAGE_DEFAULT_BPS` constant so the default cannot drift between the controller and the order screen. - `HyperLiquidProvider.test.ts` now asserts the actual submitted limit price (`51000` for a `BTC` market buy at `50000` with `maxSlippageBps: 200`) for both `placeOrder` and `editOrder`. The previous tests only checked success and TIF, which is what allowed the original silent-3% bug to ship.
…im comments Address Round 4 cross-review findings: - Move the slippage block to the top of `handlePlaceOrder`, ahead of the trade-with-any-token deposit branch, so an excessive-slippage order never starts a deposit or signature flow before being rejected. - Normalize the deprecated decimal `slippage` to bps before the position-size calc, not just before the limit-price calc, so the price-staleness check inside `calculateFinalPositionSize` uses the caller's cap instead of falling back to 300 bps. - Format the estimated slippage row to two decimals so the value never renders as `3.333333%`. - Refresh the VWAP helper JSDoc to match the new base-size walk. - Strip ticket references (AC1/AC5/Jira) from inline comments; keep only short technical notes. - Add a hook test for `usePerpsMaxSlippage` covering the default source, the user-configured source, and the persist+refresh path.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d42c1b7. Configure here.
…xception Address Round 5 cross-review findings: - `estimated_slippage_pct` is sent to MetaMetrics as a number again, in line with the other percent properties. Only the UI/toast copy is passed through `.toFixed(2)` so the row still avoids `3.333333%` noise. Adds an `estimatedSlippagePctDisplay` companion memo for the formatted string. - Update `docs/perps/perps-review-antipatterns.md` and `docs/perps/perps-architecture.md` to call out the sub-second throttle carve-out used by `usePerpsEstimatedSlippage`. The estimator needs to react to user form input within the same tick, so it uses `PERFORMANCE_CONFIG.SlippageEstimateThrottleMs` rather than the 10s cadence the standard order-form price subscription uses. Deferred: - Component test for the AC5 block path. The behavior is already covered end-to-end by the slippage agentic recipe (no BTC position created after `setMaxSlippage(10)` + submit), by the 10 VWAP unit tests, and by the 3 `usePerpsMaxSlippage` hook tests, so a focused component test would duplicate coverage. - Core sync validation against the perps-controller publish artifact. That gate runs in a separate publish workflow and requires the dist build plus a CHANGELOG entry in the `core` repo, neither of which belongs in this branch.
Replace the bare `10000` literal in the placeOrder/editOrder shim with the existing `BASIS_POINTS_DIVISOR` import so the conversion matches the convention used elsewhere in this file (USDH swap slippage, builder-fee discount math).
…test - Use `BASIS_POINTS_DIVISOR` in `calculateEstimatedSlippageBps` instead of a bare `10000` literal so the bps→decimal conversion matches the convention used by the HyperLiquid provider and the controller fee math. - Format the `max` percent in the slippage-exceeds-max toast to two decimals (was unformatted while `est` already used `.toFixed(2)`). - Source `PERPS_SLIPPAGE_DEFAULT_BPS` from `ORDER_SLIPPAGE_CONFIG.DefaultMarketSlippageBps` so the order screen default and the controller default cannot drift apart. - Drop the unnecessary `rerender()` call from `usePerpsMaxSlippage` test (the type signature requires a props argument and `act` already flushes the state update). - Add a `PerpsOrderView` component test that mocks an estimated slippage of 5% against a 1% cap, presses Place Order, and asserts `placeOrder` is not called while the validation-error toast fires. Covers the AC5 submit-block path that previously had no checked-in component coverage.
… nits Resolves Round 8 cross-review findings: - The new `PerpsOrderView` slippage-block test was failing when run as part of the full file because the surrounding suite mutates several shared mocks (`usePerpsOrderContext`, `usePerpsOrderValidation`, the slippage hooks). Restore each mock the block path reads in the new `beforeEach`, and tighten the assertion to the AC invariant — an excessive-slippage order must not reach `placeOrder`. The toast copy and analytics payload are covered by the agentic recipe and event constants. - Use `BASIS_POINTS_DIVISOR` for the bps → decimal conversion in `calculateOrderPriceAndSize` instead of a bare `10000` literal, in line with the same change applied to the slippage estimator. - Include `max_slippage_source` on the `slippage_config_changed` interaction event so the analytics payload matches the other slippage events.
- `usePerpsMaxSlippage` now reads `PERPS_SLIPPAGE_DEFAULT_BPS` from the UI's `slippageConfig` re-export so there is a single import path to the default value (the constant itself is sourced from the controller). - Inline comment next to the existing `eslint-disable-next-line react-hooks/exhaustive-deps` so the intent (revision counter forces re-read of `Engine.context`) is visible without scrolling. - Add a `HyperLiquidProvider.placeOrder` regression test that exercises the deprecated decimal `slippage: 0.02` shim and asserts the same buffered limit price as the equivalent `maxSlippageBps: 200` order, so the publisher back-compat path cannot silently regress.
CI gates require maintainer attentionBoth reviewers (Codex + Claude critic) APPROVE commit
All AC1–AC5 acceptance criteria are shipped, the agentic recipe passes 36/36 on device, and bugbot threads are resolved. |
geositta
left a comment
There was a problem hiding this comment.
Approve. The slippage value is persisted through the controller, the market-order cap is passed through to the provider as basis points, and the UI blocks submission only when the live estimate exceeds the configured cap. Focused slippage tests pass locally.
…ps-slippage-controls
…-controls' into feat/tat-1043-add-perps-slippage-controls
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection:
Tag selection rationale:
The order calculation refactor (decimal slippage → bps) is a behavioral change that could affect live order placement, making this high risk. Performance Test Selection: |
|



Description
Add user-configurable max slippage and live estimated slippage to the Perps order screen for market orders, with a client-side block when the estimate exceeds the user's cap. Matches HyperLiquid's native UX (max slippage applies to market orders placed from the order form only).
What this PR ships
Max slippagerow on the order form, visible on market orders only. Tapping opens a config bottom sheet with quick-pick presets (0.5%, 2%, 3%) and a keypad-driven custom sheet (range 0.1%–10%, 10 bps step).PerpsControllerstate (basis points, default 300 bps = 3%).Est: X% / Max: Y%.slippage_limit_blocked_orderevent fires.maxSlippageBpsis wired through to the HyperLiquid order: market-order limit price =currentPrice * (1 ± maxSlippageBps / 10000). The first commit on this branch fixed a trap where the order screen set bps while the limit-price math read a separate (always-undefined)slippagedecimal field, so the user's setting silently fell back to a hardcoded 3%. The second commit collapses both fields into a singlemaxSlippageBps.slippage_config_opened,slippage_config_changed, and newslippage_limit_blocked_orderinteraction types;max_slippage_pct,max_slippage_source(default/user_configured), andestimated_slippage_pctproperties.All slippage values are stored internally as basis points (integers). Display converts to percentage only at the UI boundary.
How slippage is applied (parity with HyperLiquid native)
maxSlippageBps, default 300 bps (3%)DefaultTpslSlippageBpsDefaultMarketSlippageBpsClose-position slippage is hardcoded and out of scope for this story per Jira §4: "Slippage on position close / add margin flows" is explicitly excluded. Alignment to HL's 8% can be a small follow-up if desired.
Acceptance criteria coverage
Out of scope (per Jira §4)
Changelog
CHANGELOG entry: Added estimated slippage and a configurable max slippage preference for perps market orders, with submission blocked when the estimate exceeds the configured cap.
Related issues
Fixes: https://consensyssoftware.atlassian.net/browse/TAT-1043
Manual testing steps
Screenshots/Recordings
Before
Slippage UI was a single
Max slippagerow inside the leverage/details box with an inline text-input bottom sheet — no preset chips, no DS chip styling, and the user-configured value did not reach HyperLiquid (silent 3% fallback).After
Est: 0% / Max: 3% ✏️. BodySM size hierarchy on Margin / Liquidation / Slippage / Fees. Flex spacer pins the info block to the bottom of the scroll view.ButtonFilterchips0.5% / 2% / 3%(3% active), trailing edit pill,Setfooter.Use custom slippage. Centered value with blinking cursor, evenly spaced−/+ButtonIconcontrols, full keypad,Cancel/Setfooter. Snaps and clamps to 10–1000 bps in 10 bps steps.Est: 0% / Max: 5% ✏️. Persistence verified via recipe: controller persists 500 bps after Set.Validation Recipe
recipe.json — slippage UI visibility, config sheet, persistence, default value
{ "title": "TAT-1043: Slippage visualization and configuration", "schema_version": 1, "description": "Validates slippage UI on perps market order: default 3% max slippage, slippage row visible, config sheet opens and changes value, persistence of the user's selection.", "validate": { "workflow": { "pre_conditions": ["wallet.unlocked", "perps.ready_to_trade"], "setup": [ { "id": "setup-nav-home", "action": "navigate", "target": "PerpsHomeView" } ], "entry": "ensure-testnet", "nodes": { "ensure-testnet": { "action": "call", "ref": "perps/setup-testnet", "next": "check-default-slippage" }, "check-default-slippage": { "action": "eval_sync", "expression": "(function(){var ctrl=Engine.context.PerpsController;var val=ctrl.getMaxSlippage();return JSON.stringify({defaultBps:val===undefined?300:val,isDefault:val===undefined||val===300})})()", "assert": { "operator": "eq", "field": "isDefault", "value": true }, "next": "clear-btc-position" }, "clear-btc-position": { "action": "eval_async", "expression": "Engine.context.PerpsController.getPositions().then(function(ps){var p=ps.find(function(x){return x.symbol==='BTC'});if(!p)return JSON.stringify({cleared:true});return Engine.context.PerpsController.closePosition({symbol:'BTC'}).then(function(){return JSON.stringify({cleared:true})})})", "assert": { "operator": "eq", "field": "cleared", "value": true }, "next": "wait-btc-clear" }, "wait-btc-clear": { "action": "wait", "duration_ms": 2000, "next": "nav-to-btc" }, "nav-to-btc": { "action": "wait_for", "test_id": "perps-market-row-item-BTC", "timeout_ms": 8000, "next": "press-btc-row" }, "press-btc-row": { "action": "press", "test_id": "perps-market-row-item-BTC", "next": "wait-side-button" }, "wait-side-button": { "action": "wait_for", "test_id": "perps-market-details-long-button", "timeout_ms": 8000, "next": "press-long" }, "press-long": { "action": "press", "test_id": "perps-market-details-long-button", "next": "wait-amount" }, "wait-amount": { "action": "wait_for", "test_id": "perps-amount-display-touchable", "timeout_ms": 10000, "next": "press-amount" }, "press-amount": { "action": "press", "test_id": "perps-amount-display-touchable", "next": "wait-keypad" }, "wait-keypad": { "action": "wait_for", "test_id": "perps-order-view-keypad", "timeout_ms": 5000, "next": "clear-keypad" }, "clear-keypad": { "action": "clear_keypad", "count": 8, "next": "type-amount" }, "type-amount": { "action": "type_keypad", "value": "10", "next": "press-done" }, "press-done": { "action": "press", "test_id": "perps-order-view-keypad-done", "next": "wait-order-form" }, "wait-order-form": { "action": "wait_for", "test_id": "perps-order-view-place-order-button", "timeout_ms": 15000, "next": "wait-slippage-row" }, "wait-slippage-row": { "action": "wait_for", "test_id": "perps-order-view-slippage-value", "timeout_ms": 10000, "next": "screenshot-row" }, "screenshot-row": { "action": "screenshot", "filename": "evidence-slippage-visible.png", "note": "Market order form showing slippage row (3% default)", "next": "check-max-display" }, "check-max-display": { "action": "wait_for", "test_id": "perps-order-view-slippage-row", "timeout_ms": 5000, "next": "open-config" }, "open-config": { "action": "press", "test_id": "perps-order-view-slippage-row", "next": "wait-config-sheet" }, "wait-config-sheet": { "action": "wait_for", "test_id": "perps-slippage-config-input", "timeout_ms": 5000, "next": "screenshot-config" }, "screenshot-config": { "action": "screenshot", "filename": "evidence-slippage-config-sheet.png", "note": "Slippage config bottom sheet open with input field and quick-pick presets", "next": "change-value" }, "change-value": { "action": "set_input", "test_id": "perps-slippage-config-input", "value": "5", "next": "save-value" }, "save-value": { "action": "press", "test_id": "perps-slippage-config-save", "next": "wait-sheet-close" }, "wait-sheet-close": { "action": "wait", "duration_ms": 1000, "next": "verify-persisted" }, "verify-persisted": { "action": "eval_sync", "expression": "(function(){var ctrl=Engine.context.PerpsController;var val=ctrl.getMaxSlippage();return JSON.stringify({bps:val,isPersisted:val===500})})()", "assert": { "operator": "eq", "field": "isPersisted", "value": true }, "next": "screenshot-updated" }, "screenshot-updated": { "action": "screenshot", "filename": "evidence-slippage-changed.png", "note": "Order form now shows max slippage updated to 5% after config change", "next": "restore-default" }, "restore-default": { "action": "eval_sync", "expression": "(function(){Engine.context.PerpsController.setMaxSlippage(300);var val=Engine.context.PerpsController.getMaxSlippage();return JSON.stringify({restored:val===300})})()", "assert": { "operator": "eq", "field": "restored", "value": true }, "next": "done" }, "done": { "action": "end", "status": "pass" } }, "teardown": [ { "id": "teardown-restore-slippage", "action": "eval_sync", "expression": "(function(){Engine.context.PerpsController.setMaxSlippage(300);return JSON.stringify({clean:true})})()", "assert": { "operator": "not_null" } }, { "id": "teardown-nav-home", "action": "navigate", "target": "PerpsHomeView" } ] } } }Validation Logs
Command:
Full output (all steps passed)
Pre-merge author checklist
Performance checks (if applicable)
Pre-merge reviewer checklist
Note
High Risk
High risk because it changes market-order execution parameters (limit-price buffering) and introduces a new client-side block path that can prevent order submission; it also adds persisted controller state used during trading.
Overview
Adds a Slippage row to the Perps order form (market orders only) showing live VWAP-based estimated slippage from the L2 order book alongside a user-configurable max cap, and opens a new bottom-sheet flow (quick-picks + custom keypad) to update that cap.
Persists
maxSlippageBpsinPerpsControllerwith newgetMaxSlippage/setMaxSlippageactions (clamped/snapped to bounds), wires the cap through order placement/editing to HyperLiquid viamaxSlippageBps(with backward-compatible normalization of deprecated decimalslippage), and blocksplaceOrderwhen the estimate exceeds the configured cap (toast + new MetaMetrics properties/events).Reviewed by Cursor Bugbot for commit df3225c. Bugbot is set up for automated code reviews on this repo. Configure here.