Expert Rating System | PO Task
A document enabling the development team to begin MVP implementation without additional business logic clarifications | By Yevhenii Holovei
Problem
Subscribers cannot objectively evaluate tipster quality. Without a unified rating, they follow popular rather than effective experts -- and lose money.
Without a transparent quality metric the portal cannot differentiate tipsters. Reputational risk grows. Advertising monetization of profiles is limited without proven effectiveness.
Scope
SCORE calculation for each active tipster based on 1X2 market | Public TOP-N leaderboard page | Profile badge: rank, SCORE, bet count, Win Rate, ROI | Daily recalculation at 04:00 CET | Data preparation rules | Anti-manipulation protection with logging
Profile verification | Sub-ratings by league and sport | Subscriber alerts on rank changes | Manual moderation / appeals | Rating based on real-money bets | Exclusion threshold for gross manipulation
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 |
Risks
Fallback to previous rating
Late bet and duplicate detector
Change without deploy