feat(settings): forward-test (paper-trading) settings section

Surfaces the PaperTradingWorker's config on the Settings page — Enabled toggle,
min-score, flat-stake, and poll interval — so forward-testing can be switched on
and tuned from the UI instead of hand-editing committed appsettings.json.

Uses the established UI-mirror-options pattern (PaperTradingSettingsForm bound to
the "PaperTrading" section, same as the UI WorkerOptions mirror) and writes through
the existing ISettingsWriter to appsettings.Local.json. Notifications is deliberately
NOT surfaced: its section holds the Telegram secret and the section-replace writer
would clobber the token — that section stays Local.json-only by design.

- PaperTradingSettingsForm (no secrets) + DI binding; Settings section + en/ru resx.
This commit is contained in:
2026-05-29 02:38:20 +03:00
parent 39aef449f7
commit 76306ef59b
5 changed files with 78 additions and 0 deletions
+37
View File
@@ -6,6 +6,7 @@
@inject IOptionsMonitor<WorkerOptions> WorkerOpts
@inject IOptionsMonitor<StorageOptions> StorageOpts
@inject IOptionsMonitor<AnomalyOptions> AnomalyOpts
@inject IOptionsMonitor<PaperTradingSettingsForm> PaperTradingOpts
@inject IOptionsMonitor<Marathon.UI.Services.LocalizationOptions> LocaleOpts
@inject ISettingsWriter Writer
@inject IDialogService Dialogs
@@ -157,6 +158,33 @@
</div>
</article>
@* FORWARD-TEST (PAPER TRADING) *@
<article class="m-section m-rise m-rise-5">
<header class="m-section__head">
<h2>@L["Settings.Section.PaperTrading"]</h2>
<MudButton Variant="Variant.Text" Size="Size.Small"
OnClick="@(() => ResetSectionAsync(PaperTradingSettingsForm.SectionName))">
@L["Settings.Action.Reset"]
</MudButton>
</header>
<div class="m-section__body">
<Field Label="@L["Settings.PaperTrading.Enabled"]" Hint="@L["Settings.PaperTrading.Enabled.Hint"]">
<MudSwitch T="bool" @bind-Value="_paperTrading.Enabled" Color="Color.Primary" />
</Field>
<Field Label="@L["Settings.PaperTrading.MinScore"]" Hint="@L["Settings.PaperTrading.MinScore.Hint"]">
<MudNumericField T="decimal" @bind-Value="_paperTrading.MinScore" Min="0m" Max="1m" Step="0.05m" Variant="Variant.Outlined" />
</Field>
<Field Label="@L["Settings.PaperTrading.FlatStake"]" Hint="@L["Settings.PaperTrading.FlatStake.Hint"]">
<MudNumericField T="decimal" @bind-Value="_paperTrading.FlatStake" Min="0.01m" Step="5m" Variant="Variant.Outlined" />
</Field>
<Field Label="@L["Settings.PaperTrading.PollIntervalSeconds"]">
<MudNumericField T="int" @bind-Value="_paperTrading.PollIntervalSeconds" Min="5" Max="3600" Variant="Variant.Outlined" />
</Field>
<SectionFooter OnSave="@(() => SaveSectionAsync(PaperTradingSettingsForm.SectionName, _paperTrading))" />
</div>
</article>
@* LOCALIZATION *@
<article class="m-section m-rise m-rise-5">
<header class="m-section__head">
@@ -184,6 +212,7 @@
private WorkerOptions _workers = new();
private StorageOptions _storage = new();
private AnomalyOptions _anomaly = new();
private PaperTradingSettingsForm _paperTrading = new();
private LocalizationOptions _locale = new();
private string _userAgentsRaw = string.Empty;
@@ -218,6 +247,14 @@
DetectionIntervalSeconds = AnomalyOpts.CurrentValue.DetectionIntervalSeconds,
};
_paperTrading = new PaperTradingSettingsForm
{
Enabled = PaperTradingOpts.CurrentValue.Enabled,
MinScore = PaperTradingOpts.CurrentValue.MinScore,
FlatStake = PaperTradingOpts.CurrentValue.FlatStake,
PollIntervalSeconds = PaperTradingOpts.CurrentValue.PollIntervalSeconds,
};
_locale = new LocalizationOptions { DefaultCulture = LocaleOpts.CurrentValue.DefaultCulture };
}
@@ -97,6 +97,14 @@
<data name="Settings.Section.Workers"><value>Background workers</value></data>
<data name="Settings.Section.Storage"><value>Storage</value></data>
<data name="Settings.Section.Anomaly"><value>Anomaly detector</value></data>
<data name="Settings.Section.PaperTrading"><value>Forward-test (paper trading)</value></data>
<data name="Settings.PaperTrading.Enabled"><value>Enable forward-test worker</value></data>
<data name="Settings.PaperTrading.Enabled.Hint"><value>Opens a flat-stake paper bet on each live directional signal and settles it when the result lands.</value></data>
<data name="Settings.PaperTrading.MinScore"><value>Min score</value></data>
<data name="Settings.PaperTrading.MinScore.Hint"><value>Only anomalies at or above this score are paper-traded.</value></data>
<data name="Settings.PaperTrading.FlatStake"><value>Flat stake</value></data>
<data name="Settings.PaperTrading.FlatStake.Hint"><value>Stake placed on every paper bet.</value></data>
<data name="Settings.PaperTrading.PollIntervalSeconds"><value>Poll interval (sec)</value></data>
<data name="Settings.Section.Localization"><value>Localization</value></data>
<data name="Settings.Action.Reset"><value>Reset section</value></data>
<data name="Settings.Action.Save"><value>Save</value></data>
@@ -101,6 +101,14 @@
<data name="Settings.Section.Workers"><value>Фоновые задачи</value></data>
<data name="Settings.Section.Storage"><value>Хранилище</value></data>
<data name="Settings.Section.Anomaly"><value>Детектор аномалий</value></data>
<data name="Settings.Section.PaperTrading"><value>Форвард-тест (бумажная торговля)</value></data>
<data name="Settings.PaperTrading.Enabled"><value>Включить воркер форвард-теста</value></data>
<data name="Settings.PaperTrading.Enabled.Hint"><value>Открывает ставку фиксированным стейком на каждый живой направленный сигнал и рассчитывает её по результату.</value></data>
<data name="Settings.PaperTrading.MinScore"><value>Мин. score</value></data>
<data name="Settings.PaperTrading.MinScore.Hint"><value>В форвард-тест попадают только аномалии со score не ниже указанного.</value></data>
<data name="Settings.PaperTrading.FlatStake"><value>Фикс. стейк</value></data>
<data name="Settings.PaperTrading.FlatStake.Hint"><value>Стейк на каждую бумажную ставку.</value></data>
<data name="Settings.PaperTrading.PollIntervalSeconds"><value>Интервал опроса (сек)</value></data>
<data name="Settings.Section.Localization"><value>Локализация</value></data>
<data name="Settings.Action.Reset"><value>Сбросить раздел</value></data>
<data name="Settings.Action.Save"><value>Сохранить</value></data>
@@ -0,0 +1,24 @@
namespace Marathon.UI.Services;
/// <summary>
/// Mutable UI mirror of the host's paper-trading options, bound to the same
/// <c>PaperTrading</c> config section so the Settings page can edit the forward-test
/// worker's behaviour. Mirrors <c>Marathon.Infrastructure.Configuration.PaperTradingOptions</c>
/// (which the worker reads); keep the two in sync. Contains no secrets.
/// </summary>
public sealed class PaperTradingSettingsForm
{
public const string SectionName = "PaperTrading";
/// <summary>Master switch — when false the worker idles.</summary>
public bool Enabled { get; set; }
/// <summary>Minimum anomaly score required to open a paper bet.</summary>
public decimal MinScore { get; set; } = 0.55m;
/// <summary>Flat stake placed on every paper bet.</summary>
public decimal FlatStake { get; set; } = 10m;
/// <summary>Seconds between open/settle cycles.</summary>
public int PollIntervalSeconds { get; set; } = 60;
}
@@ -47,6 +47,7 @@ public static class UiServicesExtensions
services.Configure<AnomalyOptions>(configuration.GetSection(AnomalyOptions.SectionName));
services.Configure<StorageOptions>(configuration.GetSection(StorageOptions.SectionName));
services.Configure<ScrapingSettingsForm>(configuration.GetSection(ScrapingSettingsForm.SectionName));
services.Configure<PaperTradingSettingsForm>(configuration.GetSection(PaperTradingSettingsForm.SectionName));
// Singletons that drive UI chrome state.
services.AddSingleton<ThemeState>();