Adds a background forward-test engine that records flat-stake "paper" bets
for directional anomalies as they fire and settles them when results arrive,
measuring the detector's live, out-of-sample edge — the antidote to backtest
overfitting. The results UI is a follow-up.
- Domain: PaperBet entity (Rate>1 / Stake>0 invariants, Open factory,
SettleAgainst — Won pays stake x rate, else Lost) + AnomalyEvidenceSide.RateFor.
- Application: OpenPaperBetsUseCase (directional + score gate, dedups by
AnomalyId, picks the post-flip favourite and its locked-in rate) and
SettlePaperBetsUseCase (Won when pick == winner else Lost; ungraded events
stay open; batched result lookup).
- Infrastructure: PaperBetEntity + config (TEXT decimals, unique AnomalyId index,
Outcome index), repository, mapping, additive AddPaperBets migration, and
PaperTradingWorker (config-gated, baseline since-marker, open+settle per cycle).
- Config: PaperTradingOptions / appsettings PaperTrading (Enabled:false default).
- 25 tests: domain settlement, both use cases, and a real-SQLite round-trip
incl. the unique-AnomalyId double-open backstop.
Persist named backtest-strategy presets so a staking config (bankroll,
min-score, stake rule, flat/percent/Kelly params) can be saved, listed,
loaded back into the form, and deleted. The per-run date range is not
part of a preset.
- Domain: SavedStrategy record (name trimmed + bounded to 80 chars,
Create() factory) wrapping the pure BacktestStrategy.
- Persistence: SavedStrategyEntity + config (TEXT decimals, unique
case-insensitive NOCASE index on Name), repository, mapping, and a
hand-trimmed AddSavedStrategies migration (additive — only the new
table). Case-insensitive names mean save-by-name overwrites instead of
creating near-duplicates.
- Application: SaveStrategyUseCase (upsert by name, keeps Id+CreatedAt) +
DeleteStrategyUseCase.
- UI: presets panel on the Backtest page (load/save/delete) + service
methods; fraction<->percent round-trip; en/ru resx.
- Fix: pin Sports.Code as ValueGeneratedNever — it is the bookmaker's
natural sport id, not an autoincrement surrogate. Corrects long-standing
model-snapshot drift; the snapshot is regenerated to match the DB.
- 25 tests across all four layers: domain validation, real-SQLite
round-trip incl. case-insensitive lookup/uniqueness, the upsert use
case, and the service percent mapping.
- Add a /health ops page: snapshot freshness (last-capture-at, colour-coded), 24h vs
total snapshots + anomalies, events tracked, sports covered, and the four worker
on/off states. Nav entry under System; localized en+ru.
- New IPipelineHealthService + ISnapshotRepository.GetLatestCapturedAtAsync (max
CapturedAt via indexed ORDER BY/LIMIT 1), with a real-SQLite round-trip test.
Review follow-ups:
- (HIGH) Add real-SQLite round-trip tests for the new query methods so the load-bearing
lexical O-format date ordering is verified, not just mocked: Anomaly
ListByDateRange/CountSince, Snapshot CountSince/ListByEvents grouping, Event Query/GetMany.
- (MED) DetectAnomaliesUseCase: one SaveChanges per event instead of per anomaly.
- (LOW) Route PlacedBetRepository + ExcelExporter date bounds through SqliteDateText.
- (LOW) Backtest: reject a one-sided date range (was silently ignored).
- (LOW) Refresh stale comments after the detector fan-out.
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>
Combined-batch reviewer flagged three real blockers + two test-infra
issues across the parallel P2/P3/P5 batch. All resolved:
PHASE 3 — DateTimeOffset UTC-kind constructor (3 sites)
EventListingParserBase.cs:39, EventOddsParser.cs:72, ResultsParser.cs:104
Replaced `new DateTimeOffset(DateTimeOffset.UtcNow.UtcDateTime, MoscowOffset)`
(throws ArgumentException because UtcDateTime has Kind=Utc) with
`DateTimeOffset.UtcNow.ToOffset(MoscowOffset)`.
PHASE 2 — EF string.Compare not translatable (3 sites)
EventRepository.cs:34, SnapshotRepository.cs:46, ExcelExporter.cs:35
Replaced `string.Compare(col, str, StringComparison.Ordinal)` with
`col.CompareTo(str)` so EF Core's SQLite provider can translate the
expression. Semantics unchanged (SQLite default collation = BINARY = ordinal).
PHASE 3 — ServerTimeProvider regex misses JSON-quoted key
Regex `serverTime\s*:\s*"..."` only matched bare-key form. Updated to
`"?serverTime"?\s*:\s*"..."` so the JSON-quoted form (the actual
marathonbet.by production format) is matched.
PHASE 3 — fixture: orphan <td> elements stripped by HTML5 parser
tests/.../Fixtures/marathonbet/event-football-sample.html — wrapped
the <td> blocks in a proper <table><tbody><tr> hierarchy so AngleSharp
preserves them and `td.Closest("td")` succeeds in the parser.
PHASE 2 — InMemoryDbFixture shared state across parallel tests
All fixture instances used `Data Source=marathon_tests` causing xUnit's
parallel-within-class runs to contaminate each other's data. Each fixture
now uses a Guid-suffixed unique data source name.
PLAN.md — P2/P3/P5 rows updated to ✅ Done with batch commit reference.
Test status:
Domain.Tests: 96/96 ✅
Application.Tests: 1/1 ✅
Infrastructure.Tests: 77/77 ✅
UI.Tests: 11/11 ✅
TOTAL: 185/185 ✅
Build: 0 warnings, 0 errors.
Deferred to later phases (per reviewer 🟡 / 🔵 notes):
- SnapshotRepository.GetAsync(Guid) uses lossy GetHashCode workaround;
Phase 4 to fix or remove from interface.
- Excel Sport name column writes string.Empty (need lookup join in Phase 6).
- PeriodScopeMapper football n>2 falls through to "Quarter" token;
guarded by MaxPeriods today, but defensive cleanup at Phase 9.
- Settings.razor duplicate m-rise-5 class on Localization section.
Three minimal fixes to make Marathon.sln build with 0/0:
1. Marathon.Infrastructure.csproj — add InternalsVisibleTo for
Marathon.Infrastructure.Tests so test code can reference internal
repository and exporter classes (Phase 2 issue blocking Phase 3 tests).
2. EventOddsParserTests.cs — add 'using Marathon.Domain.ValueObjects' so
MatchScope/PeriodScope resolve.
3. RoundTripTests.cs — add 'using Microsoft.EntityFrameworkCore' so the
ExecuteSqlRawAsync extension method on DatabaseFacade resolves.
Phase 5's anticipated LocalizationOptions / Serilog issues were already
resolved by its agent before being killed — no changes needed there.
Build status: 0 warnings, 0 errors.
Test status: Domain 96/96, UI 11/11, Infrastructure 42/77 (35 failing —
parser fixture issues + a real DateTimeOffset bug; reviewer will assess).