4.8 KiB
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, returnsIReadOnlyList<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 ≥
OddsFlipThresholdAND the favorite changed (argmax differs), emit anAnomaly(Kind=SuspensionFlip, Score, EvidenceJson)whereEvidenceJsoncontains the snapshots bracketing the suspension
- Pure domain logic — takes
- Add
AnomalyOptionsPOCO bound toAnomaly:*:public sealed class AnomalyOptions { public int SuspensionGapSeconds { get; init; } = 60; public decimal OddsFlipThreshold { get; init; } = 0.30m; public int MinSnapshotCount { get; init; } = 3; } - Implement
DetectAnomaliesUseCaseinMarathon.Application/UseCases/:- Iterate over events with new snapshots since last detection run
- Invoke
AnomalyDetectorper event - Persist new anomalies via
IAnomalyRepository
- Implement
AnomalyDetectionPoller : BackgroundServiceinMarathon.Infrastructure/Workers/:- Runs every
Anomaly:DetectionIntervalSeconds(default 60s) - Calls
DetectAnomaliesUseCase
- Runs every
- 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
DetectedAtdescending - 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
- List of anomalies sorted by
- Create
Marathon.UI/Components/AnomalyCard.razor— visually distinctive, attention-grabbing without being garish; follows frontend-design guidance for information hierarchy. - Add navigation entry to
MainLayoutdrawer 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.cssrc/Marathon.Domain/AnomalyDetection/SuspensionInterval.cssrc/Marathon.Application/UseCases/DetectAnomaliesUseCase.cssrc/Marathon.Application/Configuration/AnomalyOptions.cs(or in Infra)src/Marathon.Infrastructure/Workers/AnomalyDetectionPoller.cssrc/Marathon.UI/Pages/Anomalies/AnomalyFeed.razorsrc/Marathon.UI/Components/AnomalyCard.razortests/Marathon.Domain.Tests/AnomalyDetection/AnomalyDetectorTests.cstests/Marathon.UI.Tests/Pages/Anomalies/**
Acceptance Criteria
- Compiles (Big Bang).
AnomalyDetectoris 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