using Marathon.Domain.Backtesting; using Marathon.Domain.Enums; namespace Marathon.UI.Services; /// /// Form bound by the Backtest page. Loose-typed so MudBlazor fields can bind /// raw numerics; the service translates this into a domain /// after validation. /// public sealed class BacktestForm { public decimal StartingBankroll { get; set; } = 1000m; public decimal MinScore { get; set; } = 0.45m; public StakeRule StakeRule { get; set; } = StakeRule.Flat; public decimal FlatStake { get; set; } = 50m; /// Bound to the UI as a percentage 0–100; converted to a fraction before sim. public decimal PercentOfBankrollPercent { get; set; } = 2m; /// Bound to the UI as a percentage 0–100; converted to a fraction before sim. public decimal KellyFractionPercent { get; set; } = 25m; public bool IsValid(out string? error) { if (StartingBankroll <= 0m) { error = "Bankroll must be positive."; return false; } if (MinScore is < 0m or > 1m) { error = "Min score must be in [0, 1]."; return false; } switch (StakeRule) { case StakeRule.Flat: if (FlatStake <= 0m) { error = "Flat stake must be positive."; return false; } if (FlatStake > StartingBankroll) { error = "Flat stake exceeds starting bankroll."; return false; } break; case StakeRule.PercentOfBankroll: if (PercentOfBankrollPercent is <= 0m or > 100m) { error = "Percent of bankroll must be in (0, 100]."; return false; } break; case StakeRule.Kelly: if (KellyFractionPercent is <= 0m or > 100m) { error = "Kelly fraction must be in (0, 100]."; return false; } break; } error = null; return true; } public BacktestStrategy ToStrategy() => new( StartingBankroll: StartingBankroll, MinScore: MinScore, StakeRule: StakeRule, FlatStake: FlatStake, PercentOfBankroll: PercentOfBankrollPercent / 100m, KellyFraction: KellyFractionPercent / 100m); } /// UI-facing projection of . public sealed record BacktestVm( decimal StartingBankroll, decimal FinalBankroll, decimal NetProfit, decimal? RoiPercent, decimal TotalStaked, decimal TotalReturned, decimal MaxDrawdown, decimal? MaxDrawdownPercent, int BetsPlaced, int Wins, int Losses, int Skipped, int SkippedByThreshold, int SkippedByDataQuality, int SkippedByBankroll, int MaxWinStreak, int MaxLossStreak, IReadOnlyList Trace, IReadOnlyList EquityCurve); /// /// Trace row plus pre-shaped event title for the link-back affordance. /// public sealed record BacktestTraceRow( BacktestTrace Trace, string EventTitle); /// One point on the equity curve — bankroll over time. /// When the bet would have been placed. /// Bankroll after this bet settled. public sealed record EquityPoint(DateTimeOffset DetectedAt, decimal Bankroll);