fix(social): distinguish opened/added/reduced/closed trades (TSA-988) - #34769
fix(social): distinguish opened/added/reduced/closed trades (TSA-988)#34769xavier-brochard wants to merge 1 commit into
Conversation
A trader's position moves through four lifecycle events: they open it, add to it, reduce it, and close it. The feed and the trade list only knew `intent: 'enter' | 'exit'`, so a partial exit announced "closed" — a follower saw "alice closed ETH" when alice had trimmed 10%, and the row's right column flipped to realized P&L on the strength of that same wrong boolean. The social-api's push copy already classified all four stages, so the three surfaces also disagreed with each other about the same fill. `resolveTradeActions` derives the stage for every fill in a position. It prefers the API's new per-trade `action`, computed server-side against the full history, and otherwise walks the trades locally so this ships without waiting on the API and the `@metamask/social-controllers` bump. The local walk takes each fill's sign from `intent`, never `tokenAmount` or `direction`: opening a short is `direction: 'sell'` with `intent: 'enter'` while the size is reported as a positive magnitude, so signing by intent makes longs and shorts accumulate identically. For spot the walk is anchored to `positionAmount`, because the API caps a position at 50 fills and preferentially retains commented ones — starting from zero would report fill #51 of a long-lived position as the one that opened it. Perps opt out of that anchor: they keep their historical `positionAmount` after closing, so seeding from it would fabricate a pre-window balance and read every closing fill as a reduce. Both defer to `isClosedPosition` for the newest exit, which now prefers the API's `isOpen` over its own heuristics. Perps surface all four stages; spot surfaces only the direction, Bought or Sold. A token row carries no LONG/SHORT badge, and in the feed the figure beside the verb is P&L rather than trade direction, so the verb is the only buy/sell signal it has. Spot still carries the full four-state action underneath, because that is what decides whether a row realizes P&L. The `feed_action` analytics values change with it — `bought`/`sold` become `opened`/`added`/`reduced`/`closed`, with `trade_type` still carrying spot vs perps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
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. |
PR template — items to address before "Ready for review"Warnings — informational, address before merging:
See docs/readme/ready-for-review.md for the full Definition of Ready for Review. |
🧪 Flaky unit test detectionRun history flaky detectionHistorical failure rate is a hint, not proof — review each suggestion in context. See the flaky-test-detection skill for the full pattern reference and manual audit workflow. Failures / runs sampled per window:
AI-detected flaky patterns
|
|



Description
A trader's position moves through four events: they open it, add to it, reduce it, and close it. The feed and the trade list only knew
intent: 'enter' | 'exit', so a partial exit announced "closed" — a follower saw "alice closed ETH" when alice had trimmed 10%, and the row's right column flipped to realized P&L on the strength of that same wrong boolean. The social-api's push copy already classified all four stages, so the three surfaces also disagreed with each other about the same fill.resolveTradeActions(new,app/components/Views/SocialLeaderboard/utils/tradeAction.ts) derives the stage for every fill in a position. It prefers the API's new per-tradeaction, computed server-side against the full history, and otherwise walks the trades locally — so this ships without waiting on the social-api deploy or the@metamask/social-controllersbump, and silently upgrades once they land.Three details worth a reviewer's attention:
Fills are signed by
intent, nevertokenAmountordirection. Opening a short isdirection: 'sell'withintent: 'enter'while the size is reported as a positive magnitude, so signing by intent makes longs and shorts accumulate identically and one walk covers both.The spot walk is anchored to
positionAmount. The API caps a position at 50 fills and preferentially retains commented ones, sotradescan start mid-position — accumulating from zero would report fill Introduce e2e tests via Detox #51 of a long-lived position as the one that opened it. Seeding with whateverpositionAmountcannot account for recovers the truncated prefix, with a relative tolerance so ordinary reconciliation drift doesn't fabricate one.Perps opt out of that anchor. They keep their historical
positionAmountafter closing — a closed 5x long still reports a size of 5, asperp.tsalready documented — so seeding from it would invent a pre-window balance and read every closing perp fill as a reduce. Both paths defer toisClosedPositionfor the newest exit, which now prefers the API'sisOpenover its own heuristics.Wording. Perps surface all four stages (
Opened/Added/Reduced/Closed); spot surfaces only the direction (Bought/Sold). A token row carries no LONG/SHORT badge, and in the feed the figure beside the verb is P&L rather than trade direction, so for spot the verb is the only buy/sell signal the row has. Spot still carries the full four-state action underneath, because that is what decides whether a row realizes P&L — which is the actual bug fix.Behaviour change to note.
mapFeedItempreviously usedintent === 'exit'as a deliberate override for perps carrying stale non-zero margin. Removing it is the point of the ticket, but it means that without the API'sisOpen, a full close on a stale-margin payload now reads "reduced" instead of "closed". Routine partial reduces were being mislabelled before; a stale-margin close is the rarer case, and it becomes exact once the API field ships.Analytics.
feed_actionvalues change frombought/sold/opened/closedtoopened/added/reduced/closed. Asset class is already carried bytrade_type, but downstream dashboards will need updating.Changelog
CHANGELOG entry: Fixed the trader feed and position views describing a partly reduced position as closed, and added distinct labels for adding to and reducing a position
Related issues
Fixes: https://consensyssoftware.atlassian.net/browse/TSA-988
Depends on (both additive, neither blocking this PR):
@metamask/social-controllerstypes forTrade['action']andPosition['isOpen']Manual testing steps
Screenshots/Recordings
Before
Perp position view listed every entry as
Openedand every exit asClosed, with no way to tell a trim from a full exit.After
Pre-merge author checklist
Performance checks (if applicable)
No new rendering work: the action array is resolved once per position in
useTraderPositionData/mapFeedItem, both already memoized, and each row receives its verb as a prop rather than deriving it.Pre-merge reviewer checklist
🤖 Generated with Claude Code