adr: 0034 title: "District standings rank by TOTAL member ROI (sum, not average)" status: accepted implementation-status: implemented date: 2026-06-10 implemented-date: 2026-06-10 implemented-in: - ftl-backend bug/district-roi-sum - ftl-frontend bug/district-roi-sum deciders: [@amalkrsihna] affects-specs: [leaderboard] affects-code: - ftl-backend/internal/leaderboard/service.go - ftl-frontend/src/lib/api.ts - ftl-frontend/src/pages/Ranks.tsx supersedes: null superseded-by: null
ADR-0034: District standings rank by TOTAL member ROI (sum, not average)¶
Context¶
ADR-0014 introduced district standings ranked by the average member ROI
(Σscore / activeCount), explicitly to remove population bias — a district of
100 players each up 1% should not outrank 2 players each up 40%. ADR-0033 kept
that aggregation when it redefined the individual score to cumulative per-trade
realized ROI.
On 2026-06-10 the product owner reviewed the Matchups tab and decided the opposite trade-off: the district board should be a raw points race — a district's score is the sum of what its players earned, and recruiting more active players is a legitimate way to climb. The average reads as deflating ("my trades barely move my district") and obscures total contribution.
Decision¶
District standings rank by
TotalROI = Σ(member ROI scores)— the sum of every member's cumulative per-trade realized ROI — because the district board is a participation-weighted points race by design.
DistrictStandinggainstotalROI(primary sort key + headline display);avgROIstays in the payload as a secondary "per-player quality" stat (shown in the head-to-head tale-of-the-tape).- Tiebreaker unchanged:
ActiveCount(more active = higher). - FE (Ranks Matchups tab): head-to-head cards, split bar, and the full
standings list all read
totalROI; the card label flips "avg ROI" → "total ROI"; the tape keeps an "Avg ROI / player" row. - No new Redis state —
TotalROIis theroiSumthe endpoint already computed for the average's numerator.
Consequences¶
- Positive: District score now reconciles with what members see (sum of their own ROIs); recruiting + activating players directly moves the board — matches the growth incentive the product wants pre-launch.
- Negative: Population bias is back by choice — large districts outrank small high-skill ones (the exact effect 0014 removed). A negative whale can sink a district's total. Mitigation if needed post-launch: cap per-member contribution or show both boards.
- Neutral:
avgROIremains computed/shipped; deprecatedtotalPnl/avgPnlcompat fields untouched (their removal is still Task 8).
Rollback: single sort-key + display revert; no data migration (both aggregates derive from the same zsets).
Alternatives considered¶
Alternative A: Keep average (status quo, ADR-0014)¶
Rejected by the product owner: reads as deflating and hides total contribution; the district contest is meant to reward mass participation.
Alternative B: Median member ROI¶
Outlier-robust skill measure. Rejected: even further from the "points race" mental model; harder to explain on the card.