Skip to content

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.

  • DistrictStanding gains totalROI (primary sort key + headline display); avgROI stays 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 — TotalROI is the roiSum the 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: avgROI remains computed/shipped; deprecated totalPnl/avgPnl compat 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.