Commit Graph

8 Commits

Author SHA1 Message Date
alexei.dolgolyov 0683e348ba fix(security): bound bet-notes length + harden EventId against path/control chars
Two defense-in-depth findings from the I-series security review (both safe today,
neither currently exploitable):
- AddBetForm.Notes was unbounded free-text into SQLite; add a 2000-char sanity cap
  in IsValid (covers both the add and edit paths), alongside the existing stake/rate
  caps.
- EventId only rejected empty/whitespace; now also reject path separators, '..'
  traversal, control/newline chars and over-length input so no current-or-future
  consumer that builds a path/filename/log line from an id can be tricked. The
  charset stays open for forward-compat with non-numeric bookmaker ids.
2026-05-29 14:14:12 +03:00
alexei.dolgolyov 41148a87a6 fix(anomalies): skip orphan-event rows in the feed instead of crashing
AnomalyBrowsingService.TryProject fell back to `new SportCode(0)` when an anomaly's
event was missing — but SportCode throws for 0, which would blow up the whole
feed/dashboard for that row. Anomalies have an FK to events so it was dead in
practice, but an orphaned row now degrades gracefully (skipped, like a row with
unparseable evidence). Closes the flagged latent crash.

- TryProject returns false when the event lookup misses; +1 test.
2026-05-29 13:32:19 +03:00
alexei.dolgolyov 36178e6d1b feat(anomalies): sort the feed (newest / top score / longest gap)
Adds a sort chip row to the anomaly feed — newest (default), highest score, or
longest suspension gap — replacing the fixed newest-first order. DetectedAt is the
tiebreak so order stays stable.

- AnomalySort enum + AnomalyFilter.Sort (default Newest, so existing constructions
  are unaffected); AnomalyBrowsingService applies it; feed sort chips +
  SetSort/SortLabel; en/ru resx.
- 2 tests: default newest-first + highest-score ordering.
2026-05-29 11:55:46 +03:00
alexei.dolgolyov 34cc72fd2d feat(anomalies): filter the feed by detector kind
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.
2026-05-29 11:38:06 +03:00
alexei.dolgolyov 6e12dd73c3 feat(backtest): strategy comparison (head-to-head)
New /anomalies/compare page runs every saved strategy preset over the same
window and ranks them by ROI — bets, W–L, hit-rate, net, and max drawdown side
by side, with the best ROI flagged. Auto-runs on load with an optional date-range refine.

- CompareStrategiesUseCase fans RunBacktestUseCase over saved presets (re-loads the
  anomaly set per preset — fine for the handful a user keeps; stays bug-for-bug
  identical to a single backtest run).
- StrategyComparisonService.BuildVm (pure) computes per-row hit-rate + a single
  best-by-ROI flag; nav entry + en/ru resx.
- 6 tests: use-case fan-out + BuildVm best/tie/no-bets/hit-rate.
2026-05-29 11:32:01 +03:00
alexei.dolgolyov 39aef449f7 feat(paper-trading): forward-test results page + worker hardening
Adds the read-only paper-trading page (/paper-trading): settled-only P&L KPIs
(net profit, ROI, hit rate, open count) plus a per-bet ledger table, with a
Forward-test nav entry under Analysis. PaperTradingService batches the
event-title join (no N+1) and folds settled bets into the summary.

Also hardens PaperTradingWorker (review finding): settle now runs in its own
catch so a transient settle failure can't advance the since-marker past an
open window — the window replays until its opens succeed.

- IPaperTradingService / PaperTradingService / PaperTradingVm + PaperBetRowVm.
- en/ru resx (full parity), service registration, nav entry.
- 2 service tests: empty ledger + settled-only aggregation incl. title fallback.
2026-05-29 02:33:42 +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 553db2bce3 feat(phase-6): event browsing UI — pre-match/live lists, detail page, +26 bUnit tests
Replaces PreMatch/Live placeholder pages with a shared EventListShell
(filter chips, date range, sortable virtualized-friendly table, debounced
search, live auto-refresh with odds-movement indicators) and adds a new
/events/{eventCode} detail page (asymmetric header lockup, dynamic
Match/Period tabs, Plotly.Blazor odds-over-time chart with accessible
data-table fallback, snapshot history, Excel export modal).

New primitives matching Phase 5's editorial-quant system:
- SportIcon: inline SVGs per sport (basketball=6, football=11,
  tennis=22723, hockey=43658, generic fallback)
- OddsCell: tabular mono with ▲/▼/— delta + flash on change
  (prefers-reduced-motion honored)
- OddsTimeline: Plotly.Blazor wrapper with theme-aware colors and
  <details>/<summary> data-table screen-reader fallback
- ExportDialog: From/To pickers + ExportKind radio + Esc/Enter
  keyboard, surfaces use-case errors inline
- EventListShell: shared section shell for PreMatch/Live cadence

State + service split keeps the RCL host-agnostic:
- IEventBrowsingService / EventBrowsingService — wraps repos, returns
  view-model records (EventListItem, EventDetail, EventScopeBoard,
  BetRow, OddsTimelinePoint, SnapshotHistoryEntry); pages never see
  EF or domain entities directly.
- EventBrowsingState — singleton (per-circuit in BlazorWebView) holding
  immutable PageFilter records for PreMatch and Live.

Plotly.Blazor 5.4.1 added (latest .NET 8 line; 7.x has breaking changes).
+59 RU/EN localization keys following the Phase 5 dot-segmented convention.

Tests: +26 bUnit tests (PreMatch/Live/Detail pages, OddsCell/SportIcon/
ExportDialog components, EventBrowsingState). Total 228/228 passing
(Domain 96 + Application 15 + Infrastructure 80 + UI 37; baseline 202).
Build clean (0/0).

PLAN.md: P2/P3/P5 top-level checkboxes ticked; P6 row marked Done.
2026-05-05 12:58:03 +03:00