292223174c
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>
30 lines
1.1 KiB
C#
30 lines
1.1 KiB
C#
namespace Marathon.Domain.AnomalyDetection;
|
|
|
|
/// <summary>
|
|
/// Single source of truth for the severity bucket boundaries that the UI
|
|
/// pill / badge, the Insights breakdowns, and any future reporter share.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Buckets are inclusive on the left, exclusive on the right (except High
|
|
/// which extends to 1.00 inclusive):
|
|
/// <list type="bullet">
|
|
/// <item>Low [<see cref="Low"/>, <see cref="Medium"/>)</item>
|
|
/// <item>Medium [<see cref="Medium"/>, <see cref="High"/>)</item>
|
|
/// <item>High [<see cref="High"/>, 1.00]</item>
|
|
/// </list>
|
|
/// Defined at the Domain layer so both the Application reporter and the
|
|
/// Marathon.UI severity rules consume the same numbers — re-tuning happens
|
|
/// in one place.
|
|
/// </remarks>
|
|
public static class AnomalySeverityThresholds
|
|
{
|
|
/// <summary>Lower bound of the Low bucket. Matches the detector's default flip threshold.</summary>
|
|
public const decimal Low = 0.30m;
|
|
|
|
/// <summary>Lower bound of the Medium bucket.</summary>
|
|
public const decimal Medium = 0.45m;
|
|
|
|
/// <summary>Lower bound of the High bucket.</summary>
|
|
public const decimal High = 0.60m;
|
|
}
|