8 Commits

Author SHA1 Message Date
alexei.dolgolyov f622dadf95 feat(paper-trading): forward-test ledger engine
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.
2026-05-29 02:25:54 +03:00
alexei.dolgolyov 2a0ea7b3a6 feat(backtest): saved strategy presets (strategy editor v1)
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.
2026-05-29 02:13:16 +03:00
alexei.dolgolyov 5eb3dec24b feat(ops): pipeline-health dashboard
- 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.
2026-05-29 01:32:41 +03:00
alexei.dolgolyov b67030ae7f test+chore: real-SQLite query coverage, batch detect writes, finish date centralization
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.
2026-05-29 01:25:25 +03:00
alexei.dolgolyov 1ad896b07e feat(my-bets): personal bet journal with CLV tracking
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>
2026-05-16 17:45:42 +03:00
alexei.dolgolyov c4d87b59d6 fix(initial-implementation): close P2/P3/P5 review blockers — 185/185 tests green
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.
2026-05-05 12:09:44 +03:00
alexei.dolgolyov 686550d697 fix(initial-implementation): resolve P2/P3 cross-phase build issues
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).
2026-05-05 11:35:42 +03:00
alexei.dolgolyov e4d8476782 WIP(initial-implementation): parallel batch P2/P3/P5 — code complete, unreviewed
Snapshot of the parallel batch (Phases 2 + 3 + 5) at session pause. Solution does
NOT build cleanly yet — known cross-phase compile issues remain to be resolved
before review. See plans/initial-implementation/PLAN.md "Resume Notes" section
for the exact tomorrow-morning action list.

Phase 2 (Storage):
- Repository interfaces in Marathon.Application/Abstractions
- DateRange, ExportKind, StorageOptions in Marathon.Application/Storage
- EF Core 8 + SQLite (WAL) persistence: 7 entities + configurations + 4 repos
- Hand-written InitialCreate migration (dotnet ef blocked by parallel work)
- ClosedXML ExcelExporter with exact customer-spec wide columns
- PersistenceModule.AddMarathonPersistence DI extension
- Round-trip + export tests (cannot run yet — see cross-phase issues)

Phase 3 (Scraping):
- IOddsScraper, IBetPlacer in Marathon.Application/Abstractions
- ScrapingOptions in Marathon.Infrastructure/Configuration
- MarathonbetScraper with 4 parsers (Upcoming, Live, EventOdds, Results)
- Helpers: ServerTimeProvider, PeriodScopeMapper, OutcomeCodeMapper, MoscowDateParser
- UserAgentRotatorHandler + Polly v8 resilience pipeline
- ScrapingModule.AddMarathonScraping DI extension
- GlobalUsings.cs aliases for EventId / Configuration disambiguation
- Parser tests with trimmed HTML fixtures
- ScrapeResultsAsync interim no-op (Phase 8 will replace via watch-list polling)

Phase 5 (UI shell — killed mid-final-verify, assumed ~95%):
- Marathon.UI populated: MainLayout, App.razor, Pages (Home, Settings),
  Components, Theme (MarathonTheme.cs + Tokens.cs + app.css), Resources
  (SharedResource.{cs,ru.resx,en.resx}), Services (ISettingsWriter), wwwroot
- WPF host: App.xaml(.cs), MainWindow.xaml(.cs), Marathon.Hosts.WpfBlazor.csproj
  with Microsoft.AspNetCore.Components.WebView.Wpf + MudBlazor + Serilog
- appsettings.json + appsettings.Development.json with all sections wired
- bUnit tests: MainLayoutTests, LocaleSwitcherTests, ThemeToggleTests,
  JsonSettingsWriterTests + Support helpers

Cross-phase issues to resolve at next session:
1. Phase 2 repository classes are 'internal' — Phase 3's tests can't reference
   them. Fix: add InternalsVisibleTo to Marathon.Infrastructure.csproj.
2. Phase 5: LocalizationOptions namespace ambiguity (AspNetCore vs Extensions).
3. Phase 5: WpfBlazor Serilog API mismatch.

Reviewer has NOT run on this batch. Move to Phase 4 only after build is green
and a combined parallel-batch reviewer passes.
2026-05-05 01:56:53 +03:00