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:
@@ -318,4 +318,46 @@
|
||||
<data name="Results.Loader.Progress.Failed"><value>Ошибка</value></data>
|
||||
<data name="Results.Loader.Summary.Format"><value>Загружено {0}, пропущено {1}, всего обработано {2}.</value></data>
|
||||
<data name="Results.Loader.Empty.NoCandidates"><value>Нет событий для загрузки в этом диапазоне.</value></data>
|
||||
|
||||
<data name="Nav.Insights"><value>Калибровка</value></data>
|
||||
<data name="Insights.Kicker"><value>Калибровка</value></data>
|
||||
<data name="Insights.Title"><value>Угадывают ли флипы победителя?</value></data>
|
||||
<data name="Insights.Lede"><value>Каждая зафиксированная аномалия suspension-flip сопоставлена с итогом матча. Hit rate показывает, оказался ли пост-флип фаворит реальным победителем — это и есть единственная метрика, говорящая, что детектор работает.</value></data>
|
||||
<data name="Insights.Stat.HitRate"><value>Hit rate</value></data>
|
||||
<data name="Insights.Stat.HitRate.Hint"><value>Пост-флип фаворит выиграл.</value></data>
|
||||
<data name="Insights.Stat.Resolved"><value>Подтверждены</value></data>
|
||||
<data name="Insights.Stat.Resolved.Hint"><value>Аномалии с известным итогом.</value></data>
|
||||
<data name="Insights.Stat.Unresolved"><value>Без результата</value></data>
|
||||
<data name="Insights.Stat.Unresolved.Hint"><value>Ждём окончания матча.</value></data>
|
||||
<data name="Insights.Stat.Hits"><value>Попадания</value></data>
|
||||
<data name="Insights.Stat.Misses"><value>Промахи</value></data>
|
||||
<data name="Insights.Stat.Total"><value>Всего аномалий</value></data>
|
||||
<data name="Insights.Section.BySeverity"><value>По уровню</value></data>
|
||||
<data name="Insights.Section.BySport"><value>По виду спорта</value></data>
|
||||
<data name="Insights.Section.ByScore"><value>По уверенности</value></data>
|
||||
<data name="Insights.Section.Resolved"><value>Подтверждённые аномалии</value></data>
|
||||
<data name="Insights.Section.Unresolved"><value>Ожидают итога</value></data>
|
||||
<data name="Insights.Column.DetectedAt"><value>Замечено</value></data>
|
||||
<data name="Insights.Column.Match"><value>Матч</value></data>
|
||||
<data name="Insights.Column.Sport"><value>Вид спорта</value></data>
|
||||
<data name="Insights.Column.Score"><value>Score</value></data>
|
||||
<data name="Insights.Column.PreFavourite"><value>До флипа</value></data>
|
||||
<data name="Insights.Column.PostFavourite"><value>После флипа</value></data>
|
||||
<data name="Insights.Column.Winner"><value>Победитель</value></data>
|
||||
<data name="Insights.Column.Outcome"><value>Вердикт</value></data>
|
||||
<data name="Insights.Column.Bucket"><value>Группа</value></data>
|
||||
<data name="Insights.Column.HitRate"><value>Hit rate</value></data>
|
||||
<data name="Insights.Column.HitsOfTotal"><value>Попаданий / всего</value></data>
|
||||
<data name="Insights.Outcome.Hit"><value>Попадание</value></data>
|
||||
<data name="Insights.Outcome.Miss"><value>Промах</value></data>
|
||||
<data name="Insights.Outcome.Unresolved"><value>Ожидает</value></data>
|
||||
<data name="Insights.Side.Side1"><value>Сторона 1</value></data>
|
||||
<data name="Insights.Side.Side2"><value>Сторона 2</value></data>
|
||||
<data name="Insights.Side.Draw"><value>Ничья</value></data>
|
||||
<data name="Insights.Side.Unknown"><value>—</value></data>
|
||||
<data name="Insights.Empty.None"><value>Аномалии ещё не зафиксированы. Когда детектор отметит первую и матч завершится, его вердикт появится здесь.</value></data>
|
||||
<data name="Insights.Empty.NoneResolved"><value>Аномалии есть, но ни у одного из их событий нет результата. Запустите загрузчик результатов или подождите окончания матчей.</value></data>
|
||||
<data name="Insights.Action.Refresh"><value>Обновить</value></data>
|
||||
<data name="Insights.Action.OpenAnomaly"><value>Открыть</value></data>
|
||||
<data name="Insights.Bucket.NotApplicable"><value>—</value></data>
|
||||
</root>
|
||||
|
||||
Reference in New Issue
Block a user