Expert Rating System | PO Task
A document enabling the development team to begin MVP implementation without additional business logic clarifications | By Yevhenii Holovei
Problem
Scope
Rating Algorithm Approach
Approach Comparison
| Approach | Pros | Cons | Verdict |
|---|---|---|---|
| Win Rate Leaderboard | Simple to implement | Easy to explain | Intuitive | Small samples (2/2 = 100%) | Ignores odds | Incentivizes favorites | Rejected |
| Bayesian ROI Score | Reflects real value | Neutralizes small samples | Manipulation-resistant | Harder to explain | Sensitive to data quality | Requires tuning | Selected |
Rationale
Algorithm Details
Win / Lose / Push
The system uses a flat betting unit model. Each prediction is treated as exactly 1 unit staked.
| Outcome | profit_i |
|---|---|
| WIN | odds_at_publish − 1 |
| LOSE | −1 |
| PUSH | 0 (counts toward N, lowering ROI) |
ROI_raw = Σ(profit_i) / NExample: 2 win (+2.50, +0.80), 2 lose (−1.00, −1.00), 1 push (0.00) → Sum = +1.30, N = 5 → ROI_raw = 0.26
How Odds Are Factored In
| Bet | Odds | Outcome | Profit |
|---|---|---|---|
| A | 1.2 | WIN | +0.2 |
| B | 3.5 | WIN | +2.5 |
| C | 5.0 | LOSE | −1 |
| D | 1.1 | LOSE | −1 |
Bets C and D each lose exactly −1 despite vastly different odds. An expert with 90% WR at avg_odds = 1.2 will have a low ROI -- the system naturally penalizes manipulation via "safe" bet selection.
Small Sample (Bayesian Correction)
PRIOR_N = 10
Confidence = N / (N + PRIOR_N)
SCORE = Confidence × ROI_raw| N | Confidence | Effect |
|---|---|---|
| 2 | 2/12 = 0.17 | Even 100% WR yields Score ≈ 0.39 |
| 50 | 50/60 = 0.83 | Rating almost entirely reflects actual ROI |
Anti-Manipulation Measures
Record rejected → rejection_log (reason='late_bet'). >5 rejections within 30 days → moderator flag.
Only the first is kept (MIN published_at). Others → rejection_log (reason='duplicate'). >3 in 7 days → flag.
ROI naturally penalizes low-odds strategies. No separate flag implemented in MVP.
Database
expert_ratings Table Fields
| Field | Type | Example | Note |
|---|---|---|---|
expert_id | VARCHAR(50) | "C" | PK |
n_valid_bets | INTEGER | 9 | For Confidence |
wins | INTEGER | 7 | For Win Rate on UI |
losses | INTEGER | 2 | Audit |
pushes | INTEGER | 0 | Reserved |
sum_profit | DECIMAL(10,4) | 6.0400 | For ROI |
roi_raw | DECIMAL(10,4) | 0.6711 | For UI and audit |
score | DECIMAL(10,4) | 0.3179 | Sort basis |
avg_odds | DECIMAL(5,2) | 2.30 | For profile |
rank | INTEGER | 2 | For quick lookup |
is_qualified | BOOLEAN | TRUE | Public TOP filter |
last_calculated_at | TIMESTAMP | 2026-03-20 06:00 CET | Freshness |
Computed on-the-fly (not stored)
Decomposition
Epic: Expert Rating System
User Stories
Acceptance Criteria -- Manipulation Detector
Given / When / Then
Edge Cases
Exactly at match start → reject (late, non-strict ≥ inequality)
event_status = 'cancelled' → rejection_log reason='event_cancelled'. Do not count as manipulation.
Keep the one with the lower prediction_id. Log both in rejection_log.
Reject both predictions with reason='invalid_event_data'. Do not generate a manipulation flag.
rapid_submission_count ≥ 10 within 60 seconds → moderation_flags severity='critical'
Definition of Done
Integration & Edge States
Where the Rating is Displayed
Edge States
| Situation | System Behavior |
|---|---|
| New expert N=0 | Profile without rating metrics. Badge "Insufficient data (0 predictions)". Not included in leaderboard. |
| N < MIN_BETS (1–4) | SCORE is calculated and stored in DB, but is_qualified = FALSE. Profile shows "Accumulating (N of 5)". |
| Missed data update | etl_status check → if pipeline incomplete, skip recalculation. Retain previous rating + error alert. |
| All events upcoming | Recalculation does not change the rating. last_calculated_at is preserved from the previous day. |
| Expert deleted/blocked | is_qualified = FALSE; soft delete (active=FALSE). Disappears from leaderboard, row is retained. |
Non-Functional Requirements
Performance & Scale
| Metric | Target |
|---|---|
| GET /leaderboard | < 200 ms with 1000 qualified experts (Redis, TTL 1 hr) |
| GET /expert/{id} | < 100 ms (DB read with index on expert_id) |
| Batch calc for 1000 experts | < 5 minutes |
| Predictions per year | 1000 tipsters × 365 days × 5 predictions = 1.8M rows |
| expert_ratings | 1 row per expert, < 1 MB |
| rejection_log | Up to 20 rows per prediction ≈ 30K rows/month |