Skip to content

fix(perps): Incorrect PnL and order size displayed in perp market page after SL execution#27906

Merged
abretonc7s merged 5 commits into
mainfrom
fix/perps/tat-2483-0325-1840
Mar 26, 2026
Merged

fix(perps): Incorrect PnL and order size displayed in perp market page after SL execution#27906
abretonc7s merged 5 commits into
mainfrom
fix/perps/tat-2483-0325-1840

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Description

Multi-fill trades (e.g., stop-loss orders split across multiple price levels by HyperLiquid) showed incorrect PnL and order size on the Perp Market screen and Perps Home screen. The deduplication key orderId-timestamp used to merge REST and WebSocket fills collapsed distinct fills with the same orderId+timestamp into one entry, losing size/PnL data. Fixed by extending the dedup key to orderId-timestamp-size-price, which preserves all distinct fills while still deduplicating identical entries from both sources. The Activity page was already correct (no Map-based dedup).

Changelog

CHANGELOG entry: Fixed incorrect PnL and order size for multi-fill trades on the Perp Market screen and Home screen recent activity

Related issues

Fixes: TAT-2483

Manual testing steps

Feature: Multi-fill trade aggregation consistency
  Scenario: SL execution with multiple fills shows correct aggregated values
    Given the user has an account with SL trades that executed as multiple fills
    When the user navigates to the ETH market page trades tab
    Then the PnL and order size match the Activity page values
    And the PnL and order size match the HyperLiquid UI aggregated view

Screenshots/Recordings

Before

After

Validation Recipe

Automated validation recipe (validate-recipe.sh)
{
  "pr": "27906",
  "title": "Verify aggregated PnL and order size consistency across all trade history surfaces",
  "jira": "TAT-2483",
  "acceptance_criteria": [
    "Perp Market screen displays correct aggregated PnL and order size for multi-fill trades",
    "Perps Home screen recent activity shows identical aggregated values",
    "Activity page shows the same aggregated values as the other two surfaces",
    "Multi-fill trades (same orderId + timestamp) are properly aggregated, not collapsed by dedup"
  ],
  "validate": {
    "static": ["yarn lint:tsc"],
    "runtime": {
      "pre_conditions": ["wallet.unlocked", "perps.feature_enabled"],
      "steps": [
        {
          "id": "check_multi_fills_exist",
          "description": "Verify account has multi-fill trades (prerequisite for the bug)",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getActiveProviderOrNull().getOrderFills({aggregateByTime: false}).then(function(fills) { var grouped = {}; fills.forEach(function(f) { var key = f.orderId + '_' + f.timestamp; if (!grouped[key]) grouped[key] = 0; grouped[key] = grouped[key] + 1; }); var multiCount = 0; Object.keys(grouped).forEach(function(k) { if (grouped[k] > 1) multiCount = multiCount + 1; }); return JSON.stringify({totalFills: fills.length, multiFillKeys: multiCount}); })",
          "assert": {
            "operator": "gt",
            "field": "multiFillKeys",
            "value": 0
          }
        },
        {
          "id": "verify_new_dedup_preserves_fills",
          "description": "Assert that the fixed dedup key (orderId+timestamp+size+price) preserves all fills - this fails with old key",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getActiveProviderOrNull().getOrderFills({aggregateByTime: false}).then(function(fills) { var oldKeyMap = {}; var newKeyMap = {}; fills.forEach(function(f) { var oldKey = f.orderId + '_' + f.timestamp; var newKey = f.orderId + '_' + f.timestamp + '_' + f.size + '_' + f.price; oldKeyMap[oldKey] = f; newKeyMap[newKey] = f; }); var oldCount = Object.keys(oldKeyMap).length; var newCount = Object.keys(newKeyMap).length; return JSON.stringify({totalFills: fills.length, oldKeyUnique: oldCount, newKeyUnique: newCount, oldKeyLost: fills.length - oldCount, newKeyLost: fills.length - newCount}); })",
          "assert": {
            "operator": "eq",
            "field": "newKeyLost",
            "value": 0
          }
        },
        {
          "id": "nav_market_eth",
          "description": "Navigate to ETH market page (has multi-fill SL trades)",
          "action": "flow_ref",
          "ref": "market-discovery",
          "params": { "symbol": "ETH" }
        },
        {
          "id": "wait_market_render",
          "description": "Wait for trades list to render with REST fills",
          "action": "wait",
          "ms": 5000
        },
        {
          "id": "screenshot_market",
          "description": "Capture market trades list for visual review",
          "action": "screenshot",
          "filename": "market-trades-eth"
        },
        {
          "id": "nav_activity",
          "description": "Navigate to activity page trades tab",
          "action": "flow_ref",
          "ref": "activity-view",
          "params": { "tab": "trades" }
        },
        {
          "id": "wait_activity_render",
          "description": "Wait for activity data to load",
          "action": "wait",
          "ms": 3000
        },
        {
          "id": "screenshot_activity",
          "description": "Capture activity trades for visual comparison",
          "action": "screenshot",
          "filename": "activity-trades"
        }
      ]
    }
  }
}

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs and Coding Standards
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable
  • I've documented my code using JSDoc format if applicable
  • I've applied the right labels on the PR

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.

Note

Medium Risk
Changes fill deduplication logic used to compute displayed trade history, which can affect user-visible PnL/size calculations if the new composite key has edge cases (e.g., precision/format differences for size/price). Scope is limited to Perps Home and Market fills merging plus added tests.

Overview
Fixes incorrect PnL and order size for stop-loss (multi-fill) executions by changing REST+WebSocket fill deduplication to key on orderId+timestamp+size+price (instead of just orderId+timestamp), so distinct fills at the same timestamp are no longer collapsed.

Adds/updates unit tests for usePerpsHomeData and usePerpsMarketFills to cover multi-fill preservation and to assert WebSocket data still wins for exact duplicates across sources.

Written by Cursor Bugbot for commit 68d15e8. This will update automatically on new commits. Configure here.

@abretonc7s abretonc7s added DO-NOT-MERGE Pull requests that should not be merged team-perps Perps team labels Mar 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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.

@abretonc7s

Copy link
Copy Markdown
Contributor Author

Report: TAT-2483 — Incorrect PnL and order size on Perp Market page after SL execution

Summary

Multi-fill trades (e.g., stop-loss orders split across multiple price levels) displayed incorrect PnL and order size on the Perp Market screen and Perps Home screen. The deduplication key used to merge REST and WebSocket fills collapsed distinct fills into one, losing data. The Activity page was unaffected because it doesn't use Map-based dedup.

Root Cause

File: app/components/UI/Perps/hooks/usePerpsMarketFills.ts:145,154
File: app/components/UI/Perps/hooks/usePerpsHomeData.ts:134,140

The dedup key ${fill.orderId}-${fill.timestamp} was insufficient for multi-fill trades. When HyperLiquid splits a SL/TP order across multiple fills (same orderId and timestamp but different sizes/prices), the Map kept only the last fill per key. CDP eval confirmed: 901 total fills, 13 orderId+timestamp keys had multiple fills — old key produced 888 unique keys (13 fills lost), new key orderId-timestamp-size-price produced 901 (0 lost).

Reproduction Commit

SHA: cea4fe6840debug(pr-27906): add reproduction marker for multi-fill dedup collapse

Metro log excerpt (BUG_MARKER detection):

[PR-27906] BUG_MARKER: fills collapsed by dedup - input=N output=M symbol=ETH

Changes

File Change
usePerpsMarketFills.ts Changed dedup key from orderId-timestamp to orderId-timestamp-size-price (2 occurrences)
usePerpsHomeData.ts Same dedup key fix (2 occurrences)
usePerpsMarketFills.test.ts Updated duplicate test to use identical fills; added multi-fill preservation test
usePerpsHomeData.test.ts Added multi-fill preservation test verifying aggregated size

Test Plan

Automated

  • yarn jest usePerpsMarketFills.test.ts — 20/20 passed
  • yarn jest usePerpsHomeData.test.ts — 53/53 passed
  • yarn lint:tsc — no errors
  • validate-recipe.sh — 8/8 steps passed
  • Recipe verifies: multi-fill keys exist, new dedup key loses 0 fills, market screen renders, activity screen renders

Manual

Feature: Multi-fill trade aggregation
  Scenario: SL execution with multiple fills shows correct PnL
    Given user has an account with SL trades that executed as multiple fills
    When user navigates to the ETH market page trades tab
    Then PnL and size match the Activity page values
    And PnL and size match the HyperLiquid UI aggregated view

Evidence

  • before.mp4 — recipe run on buggy code (dedup key = orderId-timestamp)
  • after.mp4 — recipe run on fixed code (dedup key = orderId-timestamp-size-price)
  • Screenshots captured by recipe: market-trades-eth.png, activity-trades.png

Ticket

@abretonc7s abretonc7s marked this pull request as ready for review March 25, 2026 11:47
@abretonc7s abretonc7s requested a review from a team as a code owner March 25, 2026 11:47
…25-1840

# Conflicts:
#	app/components/UI/Perps/hooks/usePerpsHomeData.test.ts
#	app/components/UI/Perps/hooks/usePerpsHomeData.ts
@github-actions github-actions Bot added size-M risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 92%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR fixes a bug in fill deduplication logic in two Perps hooks (usePerpsHomeData.ts and usePerpsMarketFills.ts). The deduplication key was changed from ${orderId}-${timestamp} to ${orderId}-${timestamp}-${size}-${price} to correctly handle cases where a single order (e.g., a stop-loss) is split into multiple partial fills with the same orderId and timestamp but different size/price values. Previously, these fills were incorrectly collapsed into one, causing wrong PnL and size calculations in the activity display.

Tags selected:

  1. SmokePerps: Directly affected - the fill deduplication logic is core to Perps trading activity display (fills/trades history). This is the primary area of change.
  2. SmokeWalletPlatform: Per SmokePerps tag description, "Perps is also a section inside the Trending tab (SmokeWalletPlatform); changes to Perps views (headers, lists, full views, e.g. PerpsHomeView, PerpsMarketListView, PerpsWithdrawView) affect Trending." The hooks feed data into these views.
  3. SmokeConfirmations: Per SmokePerps tag description, "When selecting SmokePerps, also select SmokeWalletPlatform (Trending section) and SmokeConfirmations (Add Funds deposits are on-chain transactions)."

The changes are isolated to Perps-specific data processing hooks with no impact on core infrastructure, navigation, controllers, or shared components.

Performance Test Selection:
The change is a targeted bug fix to a deduplication key string in data processing hooks. The key computation is slightly more complex (adding size and price fields), but this is negligible overhead. No UI rendering changes, no new data fetching, no state management restructuring, and no impact on app startup or critical user flows that would warrant performance testing.

View GitHub Actions results

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Comment thread app/components/UI/Perps/hooks/usePerpsMarketFills.ts
@github-actions

Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
17 value mismatches detected (expected — fixture represents an existing user).
View details

@sonarqubecloud

Copy link
Copy Markdown

@abretonc7s abretonc7s enabled auto-merge March 26, 2026 07:08
@abretonc7s abretonc7s removed the DO-NOT-MERGE Pull requests that should not be merged label Mar 26, 2026
@abretonc7s abretonc7s added this pull request to the merge queue Mar 26, 2026
Merged via the queue into main with commit 5bc0539 Mar 26, 2026
112 of 117 checks passed
@abretonc7s abretonc7s deleted the fix/perps/tat-2483-0325-1840 branch March 26, 2026 14:55
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 26, 2026
@metamaskbot metamaskbot added the release-7.72.0 Issue or pull request that will be included in release 7.72.0 label Mar 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.72.0 Issue or pull request that will be included in release 7.72.0 risk-medium Moderate testing recommended · Possible bug introduction risk size-M team-perps Perps team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants