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>
225 lines
7.6 KiB
C#
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();
|
|
}
|