feat(insights): anomaly outcome validator — hit-rate calibration page
Adds a calibration dashboard that joins persisted SuspensionFlip anomalies with EventResult rows and reports whether the post-flip favourite actually won — the single metric that says whether the detector is doing its job. Domain: - AnomalyEvidenceData + AnomalyEvidenceParser to read the JSON written by AnomalyDetector without re-implementing the schema. - AnomalyOutcomeEvaluator: pure function returning Hit / Miss / Unresolved. Tennis-style two-way markets with a Draw winner are downgraded to Unresolved rather than silently counted as Miss. - AnomalySeverityThresholds: shared Low/Medium/High constants so the UI badge and the report buckets cannot drift. Application: - EvaluateAnomalyOutcomesUseCase orchestrates the join + aggregation. - AnomalyOutcomeReport carries totals, hit rate, three breakdowns (severity / sport / score bins) and a per-event title lookup so the UI needs no second pass over IEventRepository. - Score bins extend below 0.30 automatically when the operator lowers the detector threshold so the histogram total always equals ResolvedCount. UI: - Insights page at /anomalies/insights — hero header, 4-card KPI strip (hit rate tinted by tone), three breakdown grids with bar visualisation, drill-down tables for resolved and unresolved anomalies. Honors prefers-reduced-motion. RU + EN localisation. - Nav entry under Analysis section + chip button on the Anomaly Feed. Tests: +42 across Domain + Application (evaluator boundary cases including tennis two-way and Draw guard, score-bin edges, dynamic floor when threshold is lowered, event-title pass-through). All 324 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Marathon.Domain.AnomalyDetection;
|
||||
using Marathon.Domain.Enums;
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
@@ -83,12 +84,14 @@ public enum AnomalyFavourite
|
||||
None,
|
||||
}
|
||||
|
||||
/// <summary>Helpers for severity bucketing.</summary>
|
||||
/// <summary>Helpers for severity bucketing. Thresholds come from
|
||||
/// <see cref="AnomalySeverityThresholds"/> so the UI badges and the
|
||||
/// Application-layer outcome report agree by construction.</summary>
|
||||
public static class AnomalySeverityRules
|
||||
{
|
||||
public const decimal LowThreshold = 0.30m;
|
||||
public const decimal MediumThreshold = 0.45m;
|
||||
public const decimal HighThreshold = 0.60m;
|
||||
public const decimal LowThreshold = AnomalySeverityThresholds.Low;
|
||||
public const decimal MediumThreshold = AnomalySeverityThresholds.Medium;
|
||||
public const decimal HighThreshold = AnomalySeverityThresholds.High;
|
||||
|
||||
public static AnomalySeverity FromScore(decimal score) => score switch
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user