feat(anomaly): IAnomalyDetector seam + steam-move detector
- Introduce IAnomalyDetector; the existing flip detector implements it. - Extract MatchWinEvidence so every detector writes the identical pre/post evidence shape — the UI parser and outcome evaluator handle new kinds with no branching (steam moves get hit-rate calibrated for free). - Add SteamMoveDetector: flags a rapid one-directional implied-probability rise over a short CONTINUOUS window (no suspension gap inside it), so it never double-flags the same interval as the suspension-flip detector. - DetectAnomaliesUseCase fans out over both detectors; dedup keys on EventId+Kind so flip and steam signals persist independently. Add AnomalyKind.SteamMove + SteamMove window/threshold options. 8 detector tests.
This commit is contained in:
@@ -59,10 +59,18 @@ public sealed class DetectAnomaliesUseCase
|
||||
{
|
||||
_logger.LogInformation("DetectAnomaliesUseCase: cycle started");
|
||||
|
||||
var detector = new AnomalyDetector(
|
||||
_options.SuspensionGapSeconds,
|
||||
_options.OddsFlipThreshold,
|
||||
_options.MinSnapshotCount);
|
||||
var detectors = new IAnomalyDetector[]
|
||||
{
|
||||
new AnomalyDetector(
|
||||
_options.SuspensionGapSeconds,
|
||||
_options.OddsFlipThreshold,
|
||||
_options.MinSnapshotCount),
|
||||
new SteamMoveDetector(
|
||||
_options.SteamMoveWindowSeconds,
|
||||
_options.SteamMoveDriftThreshold,
|
||||
_options.MinSnapshotCount,
|
||||
_options.SuspensionGapSeconds),
|
||||
};
|
||||
|
||||
var events = await _eventRepo.ListAsync(ct);
|
||||
int newAnomalyCount = 0;
|
||||
@@ -96,7 +104,7 @@ public sealed class DetectAnomaliesUseCase
|
||||
var existingForEvent = existingByEvent.TryGetValue(ev.Id, out var slice)
|
||||
? slice
|
||||
: new List<Anomaly>();
|
||||
newAnomalyCount += await ProcessEventAsync(detector, ev, snapshots, existingForEvent, ct);
|
||||
newAnomalyCount += await ProcessEventAsync(detectors, ev, snapshots, existingForEvent, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -120,13 +128,17 @@ public sealed class DetectAnomaliesUseCase
|
||||
// ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private async Task<int> ProcessEventAsync(
|
||||
AnomalyDetector detector,
|
||||
IReadOnlyList<IAnomalyDetector> detectors,
|
||||
Event ev,
|
||||
IReadOnlyList<OddsSnapshot> snapshots,
|
||||
List<Anomaly> existingForEvent,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var detected = detector.Detect(ev.Id, snapshots);
|
||||
// Fan out over every detector kind; dedup below keys on EventId + Kind so the
|
||||
// flip and steam signals for one event persist independently.
|
||||
var detected = detectors
|
||||
.SelectMany(d => d.Detect(ev.Id, snapshots))
|
||||
.ToList();
|
||||
|
||||
if (detected.Count == 0)
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user