108 lines
4.8 KiB
Markdown
108 lines
4.8 KiB
Markdown
# 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<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:*`:
|
|
```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
|
|
|
|
<!-- Filled by Phase 7 implementer. -->
|