Files
Encelado/Encelado/src/Encelado.Bot/Ui/Pages/SettingsPage.xaml.cs
T
Alby96andClaude Fable 5.1 4c26fd3209 Passa ai Correlation Baskets su eToro e rimuove i motori precedenti (4.0.0)
Perché: l'utente ha chiesto un bot che operi cinque basket di coppie forex
correlate su eToro, autonomo, con ledger, feed gratuiti e apprendimento
costruito da zero, e ha deciso di eliminare tutto ciò che restava delle
gestioni precedenti (Binance, cTrader/proba, ricerca con SQLite, GBDT, RL,
TA-Lib) e di non avere approvazioni manuali sui singoli ordini.

Cosa cambia:
- nuovo Core dei basket (cross sintetici, decisore, cost gate, sizing,
  esecutore leg-risk, backtest con PSR/DSR/PBO, livelli 0-3 di apprendimento),
  adattatore eToro Public API, motore autonomo con equity stop, kill-switch,
  riconciliazione, ledger append-only, calendario e notizie con sentiment;
- modalità Paper / Demo / Live (Live con flag e frase CONFERMO LIVE);
- interfaccia rifatta: barra in alto con tre schede, dashboard con i soli
  numeri principali, fuso orario selezionabile, test di rendering in PNG;
- corretto il parser dei costi eToro (campo "value"): markup e overnight
  non venivano letti;
- strumento di ricerca ridotto a ticks / baskets / falsify con due scenari di
  costo; risultati in results/ e reports/: nessuna configurazione è
  profittevole al netto dei costi (docs/STRATEGY.md lo dice con i numeri);
- documentazione completa (STRATEGY, ML_AND_LEARNING, RUNBOOK, GLOSSARY,
  KNOWN_ISSUES, ADR-0004, ADR-0005) e catena di rilascio aggiornata.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:45:55 +02:00

225 lines
7.6 KiB
C#

using System.ComponentModel;
using System.Text.Json.Nodes;
using System.Windows;
using System.Windows.Controls;
using Encelado.Bot.Configuration;
namespace Encelado.Bot.Ui.Pages;
/// <summary>
/// The configuration, as a form. Every value the bot runs on is a field here — including
/// the ones the strategy fixes, which are shown read-only rather than hidden in prose.
/// </summary>
public partial class SettingsPage : UserControl
{
private IReadOnlyList<SettingGroup> _groups = [];
private BotConfig? _config;
public SettingsPage() => InitializeComponent();
public IUiActions? Actions { get; set; }
/// <summary>Rebuilds the form from the configuration on disk.</summary>
public void Refresh(BotConfig config, string credentialStatus, string credentialPath, string about)
{
ArgumentNullException.ThrowIfNull(config);
_config = config;
CredStatus.Text = credentialStatus;
CredPath.Text = credentialPath;
AboutText.Text = about;
LogPathBox.Text = config.Logging.ResolveDirectory();
LogHint.ToolTip = DescribeLogFiles(config.Logging);
ConfigSummary.Text = $"File: {App.ConfigPath}";
foreach (SettingGroup group in _groups)
{
foreach (SettingField field in group.Fields)
{
field.PropertyChanged -= OnFieldChanged;
}
}
_groups = SettingsCatalogue.Build(config);
foreach (SettingGroup group in _groups)
{
foreach (SettingField field in group.Fields)
{
field.PropertyChanged += OnFieldChanged;
}
}
Groups.ItemsSource = _groups;
UpdateSaveState();
}
private void OnFieldChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(SettingField.Value) or nameof(SettingField.Error))
{
UpdateSaveState();
}
}
private IEnumerable<SettingField> AllFields =>
_groups.SelectMany(static g => g.Fields);
private void UpdateSaveState()
{
int dirty = AllFields.Count(static f => f.IsDirty);
int broken = AllFields.Count(static f => f.HasError);
SaveButton.IsEnabled = dirty > 0 && broken == 0;
SaveStatus.Text = broken > 0
? $"{broken} campo/i da correggere"
: dirty == 0
? "Nessuna modifica da salvare"
: $"{dirty} modifica/e non salvate — hanno effetto al prossimo avvio";
}
private void OnRevert(object sender, RoutedEventArgs e)
{
foreach (SettingField field in AllFields)
{
field.Revert();
}
UpdateSaveState();
}
private void OnSave(object sender, RoutedEventArgs e)
{
if (_config is null)
{
return;
}
List<SettingField> changed = [.. AllFields.Where(static f => f.IsDirty)];
if (changed.Count == 0)
{
return;
}
Dictionary<string, JsonNode?> changes = [];
try
{
foreach (SettingField field in changed)
{
changes[field.Path] = field.ToJson();
}
}
catch (Exception ex) when (ex is FormatException or OverflowException or ArgumentException)
{
Warn($"Un valore non è interpretabile:\n\n{ex.Message}");
return;
}
// Validated as a whole before anything is written. Individual fields can each be
// reasonable while the combination is not — a stake above the position cap, for
// instance — and finding that out at the next start, from a file the operator
// already closed, is the worst moment to find it out.
if (!Validates(changes, out string problem))
{
Warn($"La combinazione di valori non è valida:\n\n{problem}\n\nNulla è stato salvato.");
return;
}
try
{
ConfigWriter.Apply(App.ConfigPath, changes);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
or InvalidOperationException or FileNotFoundException
or ArgumentException)
{
Warn($"Non sono riuscito a salvare:\n\n{ex.Message}");
return;
}
foreach (SettingField field in changed)
{
field.Load(field.Value);
}
UpdateSaveState();
MessageBox.Show(
Window.GetWindow(this),
$"{changed.Count} valore/i salvati in:\n{App.ConfigPath}\n\n" +
"Le modifiche hanno effetto al prossimo avvio dell'applicazione.",
"Impostazioni salvate", MessageBoxButton.OK, MessageBoxImage.Information);
}
/// <summary>
/// Applies the pending changes to a throwaway copy of the configuration and runs the
/// real validators over it.
/// </summary>
private bool Validates(Dictionary<string, JsonNode?> changes, out string problem)
{
problem = string.Empty;
string temporary = Path.Combine(
Path.GetTempPath(), $"encelado-check-{Guid.NewGuid():N}.json");
try
{
File.Copy(App.ConfigPath, temporary, overwrite: true);
ConfigWriter.Apply(temporary, changes);
BotConfig candidate = ConfigLoader.Load(temporary, out _);
// The whole validator, not a subset. Individual fields can each be
// reasonable while the combination is not — an automatic mode without its
// flag, a stake that no longer fits inside the exposure cap — and finding
// that out at the next start, from a file the operator has already closed,
// is the worst moment to find it out.
candidate.Validate();
return true;
}
catch (Exception ex) when (ex is InvalidOperationException or IOException
or ArgumentException or UnauthorizedAccessException)
{
problem = ex.Message;
return false;
}
finally
{
try
{
File.Delete(temporary);
}
catch (IOException)
{
// A leftover in the temp folder is not worth failing the save over.
}
}
}
private void Warn(string message) => MessageBox.Show(
Window.GetWindow(this), message, "Encelado", MessageBoxButton.OK, MessageBoxImage.Warning);
private static string DescribeLogFiles(LoggingOptions logging) =>
$"In questa cartella: {(string.IsNullOrWhiteSpace(logging.File) ? "nessun file (log su file disattivato)" : logging.File + " il log dell'applicazione, una tabella con ; e intestazione")}. " +
"Il ledger delle decisioni e dei basket sta in data/ledger e non si sposta.";
private void OnRestoreDefaults(object sender, RoutedEventArgs e) => Actions?.RestoreDefaults();
private void OnLogin(object sender, RoutedEventArgs e) => Actions?.ShowLogin();
private void OnLogout(object sender, RoutedEventArgs e) => Actions?.ForgetCredentials();
private void OnOpenConfig(object sender, RoutedEventArgs e) => Actions?.OpenConfigFile();
private void OnOpenStrategy(object sender, RoutedEventArgs e) => Actions?.OpenStrategyFile();
private void OnOpenData(object sender, RoutedEventArgs e) => Actions?.OpenDataFolder();
private void OnOpenLogFolder(object sender, RoutedEventArgs e) => Actions?.OpenLogFolder();
private void OnChangeLogDirectory(object sender, RoutedEventArgs e) => Actions?.ChangeLogDirectory();
}