Files
maraphon-app/plans/initial-implementation/phase-7-anomaly-detection.md
T

4.8 KiB

Phase 7: Anomaly Detection (Suspension + Flip)

Status: Not Started Parent plan: 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<OddsSnapshot> for an event, returns IReadOnlyList<Anomaly>
    • 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:*:
    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