2a0ea7b3a6
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.
52 lines
2.0 KiB
C#
52 lines
2.0 KiB
C#
using Marathon.Domain.ValueObjects;
|
|
|
|
namespace Marathon.Domain.Backtesting;
|
|
|
|
/// <summary>
|
|
/// A named, persisted <see cref="BacktestStrategy"/> — the user's reusable
|
|
/// staking preset. The wrapped <see cref="Strategy"/> carries every simulation
|
|
/// parameter (bankroll, threshold, stake rule); the date-range scope of a run
|
|
/// is deliberately NOT stored here, since that is a per-run choice rather than
|
|
/// a property of the strategy itself.
|
|
/// </summary>
|
|
/// <param name="Id">Stable identity, assigned once at creation.</param>
|
|
/// <param name="Name">
|
|
/// User-supplied label. Trimmed and bounded to <see cref="MaxNameLength"/>;
|
|
/// names are unique across the store (enforced by the persistence layer).
|
|
/// </param>
|
|
/// <param name="Strategy">The staking configuration this preset captures.</param>
|
|
/// <param name="CreatedAt">When the preset was first saved (Moscow time).</param>
|
|
public sealed record SavedStrategy(
|
|
Guid Id,
|
|
string Name,
|
|
BacktestStrategy Strategy,
|
|
DateTimeOffset CreatedAt)
|
|
{
|
|
/// <summary>Maximum length of a trimmed strategy name.</summary>
|
|
public const int MaxNameLength = 80;
|
|
|
|
public string Name { get; } = NormalizeName(Name);
|
|
|
|
/// <summary>
|
|
/// Builds a brand-new preset with a fresh identity and the current Moscow
|
|
/// timestamp. Use this for "Save"; use <c>with</c> to amend an existing one.
|
|
/// </summary>
|
|
public static SavedStrategy Create(string name, BacktestStrategy strategy)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(strategy);
|
|
return new SavedStrategy(Guid.NewGuid(), name, strategy, MoscowTime.Now);
|
|
}
|
|
|
|
private static string NormalizeName(string name)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(name);
|
|
var trimmed = name.Trim();
|
|
if (trimmed.Length == 0)
|
|
throw new ArgumentException("Strategy name must not be empty.", nameof(name));
|
|
if (trimmed.Length > MaxNameLength)
|
|
throw new ArgumentException(
|
|
$"Strategy name must be at most {MaxNameLength} characters.", nameof(name));
|
|
return trimmed;
|
|
}
|
|
}
|