34cc72fd2d
Adds a detector-kind chip row to the anomaly feed (SuspensionFlip / SteamMove / SuspensionFreeze / OverroundCompression), multi-select like the sport filter — so with four detectors live you can slice the feed to a single signal type. The kind set lives on AnomalyFilter and filters in-memory alongside severity/sport, persisted via AnomalyBrowsingState like the other filters. - AnomalyFilter.Kinds + AnomalyBrowsingService in-memory Where clause; feed chip row + ToggleKind/KindLabel; en/ru resx (Anomaly.Filter.Kind). - 2 tests: kind-filtered subset + no-filter returns all kinds.
110 lines
3.3 KiB
C#
110 lines
3.3 KiB
C#
using Marathon.Domain.AnomalyDetection;
|
|
using Marathon.Domain.Enums;
|
|
using Marathon.Domain.ValueObjects;
|
|
|
|
namespace Marathon.UI.Services;
|
|
|
|
/// <summary>
|
|
/// Severity bucket derived from <see cref="AnomalyListItem.Score"/>.
|
|
/// Phase 7 mapping (see backend handoff):
|
|
/// Low = [0.30, 0.45), Medium = [0.45, 0.60), High = [0.60, 1.00].
|
|
/// </summary>
|
|
public enum AnomalySeverity
|
|
{
|
|
Low,
|
|
Medium,
|
|
High,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Filter state passed from a page to <see cref="IAnomalyBrowsingService"/>.
|
|
/// All fields optional — empty filter returns the full feed.
|
|
/// </summary>
|
|
public sealed record AnomalyFilter(
|
|
AnomalySeverity? MinSeverity = null,
|
|
IReadOnlyCollection<int>? SportCodes = null,
|
|
DateTimeOffset? From = null,
|
|
DateTimeOffset? To = null,
|
|
IReadOnlyCollection<AnomalyKind>? Kinds = null);
|
|
|
|
/// <summary>
|
|
/// Compact anomaly row used by the feed page. Designed to render without any
|
|
/// further repository calls — pre-shaped strings + parsed evidence summary.
|
|
/// </summary>
|
|
public sealed record AnomalyListItem(
|
|
Guid Id,
|
|
EventId EventId,
|
|
string EventTitle,
|
|
SportCode Sport,
|
|
string CountryCode,
|
|
string LeagueId,
|
|
DateTimeOffset DetectedAt,
|
|
decimal Score,
|
|
AnomalySeverity Severity,
|
|
AnomalyKind Kind,
|
|
int SuspensionGapSeconds,
|
|
decimal? PreWin1Rate,
|
|
decimal? PreDrawRate,
|
|
decimal? PreWin2Rate,
|
|
decimal? PostWin1Rate,
|
|
decimal? PostDrawRate,
|
|
decimal? PostWin2Rate,
|
|
AnomalyFavourite PreFavourite,
|
|
AnomalyFavourite PostFavourite,
|
|
bool IsTwoWay);
|
|
|
|
/// <summary>
|
|
/// Full anomaly aggregate for the detail page. Carries the parsed evidence
|
|
/// snapshots plus the originating event metadata for the link-back affordance.
|
|
/// </summary>
|
|
public sealed record AnomalyDetailVm(
|
|
AnomalyListItem Item,
|
|
AnomalyEvidenceSnapshot Pre,
|
|
AnomalyEvidenceSnapshot Post);
|
|
|
|
/// <summary>
|
|
/// Snapshot bracket of the suspension window — pre or post — with raw rates,
|
|
/// implied probabilities, and the side that was the favourite at that moment.
|
|
/// </summary>
|
|
public sealed record AnomalyEvidenceSnapshot(
|
|
DateTimeOffset CapturedAt,
|
|
decimal? Rate1,
|
|
decimal? RateDraw,
|
|
decimal? Rate2,
|
|
decimal? P1,
|
|
decimal? PDraw,
|
|
decimal? P2,
|
|
AnomalyFavourite Favourite);
|
|
|
|
/// <summary>Side that holds the lowest implied probability in a snapshot.</summary>
|
|
public enum AnomalyFavourite
|
|
{
|
|
Side1,
|
|
Draw,
|
|
Side2,
|
|
None,
|
|
}
|
|
|
|
/// <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 = AnomalySeverityThresholds.Low;
|
|
public const decimal MediumThreshold = AnomalySeverityThresholds.Medium;
|
|
public const decimal HighThreshold = AnomalySeverityThresholds.High;
|
|
|
|
public static AnomalySeverity FromScore(decimal score) => score switch
|
|
{
|
|
< MediumThreshold => AnomalySeverity.Low,
|
|
< HighThreshold => AnomalySeverity.Medium,
|
|
_ => AnomalySeverity.High,
|
|
};
|
|
|
|
public static bool MeetsThreshold(AnomalySeverity actual, AnomalySeverity? minimum)
|
|
{
|
|
if (minimum is null) return true;
|
|
return (int)actual >= (int)minimum.Value;
|
|
}
|
|
}
|