# Phase 7: Anomaly Detection (Suspension + Flip) **Status:** ⬜ Not Started **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** fullstack **Implementer:** Sonnet (backend portion) + Opus (UI portion, with frontend-design) **Depends on:** Phase 4 (snapshot pipeline) + Phase 6 (UI patterns) ## Objective Detect the **odds-flip anomaly** described in customer TZ §3: bookmaker freezes betting on a live event, then re-opens with inverted underdog/favorite odds. Persist anomalies and surface them in a dedicated UI feed page so the user can act on them. ## Tasks ### Backend (Sonnet) - [ ] Implement `Marathon.Domain/AnomalyDetection/AnomalyDetector.cs`: - Pure domain logic — takes `IReadOnlyList` for an event, returns `IReadOnlyList` - Detect suspension intervals: gaps between snapshots > `SuspensionGapSeconds` (configurable) - For each suspension, compute pre-suspension and post-suspension implied probability vectors `(p1, pDraw, p2)` from Win-1/Draw/Win-2 rates - Compute flip score: `max(|p_post[i] - p_pre[i]|)` across i ∈ {1, draw, 2} - If flip score ≥ `OddsFlipThreshold` AND the favorite changed (argmax differs), emit an `Anomaly(Kind=SuspensionFlip, Score, EvidenceJson)` where `EvidenceJson` contains the snapshots bracketing the suspension - [ ] Add `AnomalyOptions` POCO bound to `Anomaly:*`: ```csharp public sealed class AnomalyOptions { public int SuspensionGapSeconds { get; init; } = 60; public decimal OddsFlipThreshold { get; init; } = 0.30m; public int MinSnapshotCount { get; init; } = 3; } ``` - [ ] Implement `DetectAnomaliesUseCase` in `Marathon.Application/UseCases/`: - Iterate over events with new snapshots since last detection run - Invoke `AnomalyDetector` per event - Persist new anomalies via `IAnomalyRepository` - [ ] Implement `AnomalyDetectionPoller : BackgroundService` in `Marathon.Infrastructure/Workers/`: - Runs every `Anomaly:DetectionIntervalSeconds` (default 60s) - Calls `DetectAnomaliesUseCase` - [ ] Backend tests in `Marathon.Domain.Tests/AnomalyDetection/`: - Synthetic snapshot timeline with no flip → 0 anomalies - Snapshot timeline with suspension + small odds shift → 0 anomalies (below threshold) - Snapshot timeline with suspension + large flip (favorite ↔ underdog) → 1 anomaly - Score calculation matches expected value ### Frontend (Opus + frontend-design) - [ ] Create `Marathon.UI/Pages/Anomalies/AnomalyFeed.razor`: - List of anomalies sorted by `DetectedAt` descending - Each card shows: severity (color-coded by score), event identity, sport icon, detected timestamp, mini sparkline of pre/post odds - Click card → expand to show evidence timeline (snapshots before/after suspension) - Filter: severity threshold, sport, date range - [ ] Create `Marathon.UI/Components/AnomalyCard.razor` — visually distinctive, attention-grabbing without being garish; follows frontend-design guidance for information hierarchy. - [ ] Add navigation entry to `MainLayout` drawer with notification badge showing unread anomaly count. - [ ] Localize all strings in RU + EN. - [ ] Frontend tests in `Marathon.UI.Tests/Pages/Anomalies/`: - bUnit: anomaly card renders evidence timeline - bUnit: filter narrows the list correctly ## Files to Modify/Create - `src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs` - `src/Marathon.Domain/AnomalyDetection/SuspensionInterval.cs` - `src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs` - `src/Marathon.Application/Configuration/AnomalyOptions.cs` (or in Infra) - `src/Marathon.Infrastructure/Workers/AnomalyDetectionPoller.cs` - `src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor` - `src/Marathon.UI/Components/AnomalyCard.razor` - `tests/Marathon.Domain.Tests/AnomalyDetection/AnomalyDetectorTests.cs` - `tests/Marathon.UI.Tests/Pages/Anomalies/**` ## Acceptance Criteria - Compiles (Big Bang). - `AnomalyDetector` is a pure function — no I/O, no DI dependencies. - Configurable thresholds via `appsettings.json` (visible in Settings page). - UI clearly distinguishes high/medium/low severity anomalies. - Evidence timeline shows the actual snapshots that triggered the detection. ## Notes - This is the **product's actual differentiator** — quality of detection logic and evidence presentation matters. Spend time getting the score formula right. - Implied probability formula: `p = 1 / odds` (then normalize so they sum to 1). - Big Bang: compile-only smoke check. ## Review Checklist - [ ] Detector is deterministic and pure - [ ] Score calculation correct (verify against hand-computed example) - [ ] No false positives on synthetic "normal" timelines - [ ] UI evidence timeline matches stored `EvidenceJson` - [ ] All strings localized ## Handoff to Next Phase