Adds a manual bet-tracking journal that turns the analyzer into an actual
bet tracker. Users record wagers; the journal auto-grades them when event
results land and computes per-bet Closing-Line-Value against the latest
pre-match snapshot — the strongest long-run indicator of betting skill.
Domain:
- PlacedBet entity (reuses Bet vocabulary for Scope/Type/Side/Value/Rate)
with stake, placed-at, outcome, and notes. Derived GrossReturn / NetProfit.
- BetOutcome enum (Pending / Won / Lost / Void).
- BetOutcomeResolver: pure function grading any Match-scope bet against an
EventResult. Handles 1X2, draws, handicap (incl. push), and totals.
Period-scope bets stay manual since EventResult only carries full-time.
Application:
- IPlacedBetRepository abstraction.
- ClosingLineValueCalculator: pure CLV math (implied-probability delta) +
snapshot-matching predicate by Scope/Type/Side/Value.
- BetJournalReport + BetJournalStats records.
- Four use cases: Record / ResolvePending / BuildReport / Delete.
- New ISnapshotRepository.GetLatestPreMatchAsync pushes the closing-line
pick into a single SQLite query rather than materialising the 30-day
window in memory per event.
- ROI turnover excludes Void stakes — pushes are not real turnover and
including them would dilute the user's edge.
Infrastructure:
- PlacedBetEntity / Configuration / Repository / Mapping helpers.
- 20260516 migration adding the PlacedBets table with EventCode and
Outcome indices. Intentionally NO foreign key to Events — the journal
is user data and must survive snapshot-retention pruning. Covered by an
explicit round-trip test.
UI:
- Pages/MyBets/Journal.razor: hero header, 4-card KPI strip (ROI / strike
rate / avg CLV / net profit, tinted by tone), inline add-bet form with
the same invariants as the Bet record, drill-down table with per-row
outcome pills, CLV percentage-points column, P&L, notes underline, and
inline-confirm delete. RU + EN i18n.
- Nav entry under Analysis.
Tests: +55 across Domain / Application / Infrastructure (resolver math
including handicap push and total push boundaries, PlacedBet invariants
and derived properties, CLV math + null-handling, four use cases under
NSubstitute, EF round-trip including survives-event-deletion). All 379
tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- AnomalyDetector (pure domain): detects odds-flip pattern from live snapshot
timelines using implied-probability vectors (p=1/rate, normalised), flip score
= max(|p_post−p_pre|), gated by both threshold AND favourite-changed test
- SuspensionInterval record: typed pair of (pre, post) OddsSnapshot bracketing a gap
- AnomalyOptions POCO (Application layer): bound to Anomaly:* config section with
four fields (SuspensionGapSeconds=60, OddsFlipThreshold=0.30, MinSnapshotCount=3,
DetectionIntervalSeconds=60)
- DetectAnomaliesUseCase: iterates all events, loads last-24h live snapshots, runs
detector, persists new anomalies with 1-minute dedup window
- AnomalyDetectionPoller: BackgroundService polling every DetectionIntervalSeconds,
gated by WorkerOptions.AnomalyDetectionEnabled (default true)
- DI wiring: DetectAnomaliesUseCase registered Scoped in ApplicationModule;
AnomalyOptions bound + AnomalyDetectionPoller hosted in InfrastructureModule
- WorkerOptions.AnomalyDetectionEnabled added; appsettings.json updated
- 13 domain tests + 4 application tests; total 245/245 passing (no regression)