diff --git a/Mimante/App.xaml.cs b/Mimante/App.xaml.cs
index 71252a8..9609d66 100644
--- a/Mimante/App.xaml.cs
+++ b/Mimante/App.xaml.cs
@@ -23,32 +23,30 @@ namespace AutoBidder
// Le impostazioni stanno nella radice fissa, quindi si possono leggere anche
// prima di sapere dove finiranno gli altri dati.
var settings = SettingsManager.Load();
- AppPaths.Configure(settings.DataFolder, settings.StatsFolder, settings.LogFolder, settings.DatabasePath);
+ AppPaths.Configure(settings.DatabaseFolder);
AppPaths.EnsureFolders();
- // Le versioni precedenti scrivevano parte dei dati in Roaming e parte in Local,
- // senza sottocartelle: senza questo passaggio storico e aste sparirebbero.
- AppPaths.MigrateLegacyFiles();
-
- // I registri su file partono subito: quello che succede durante l'avvio è
- // proprio ciò che non si riesce a leggere a video.
- TextLogService.SessionStarted(AppInfo.Version);
- TextLogService.PurgeOldLogs();
- TextLogService.App("INFO", $"[PERCORSI] configurazione in {AppPaths.ConfigRoot}, dati in {AppPaths.DataFolder}");
-
- // Il database si apre per primo: da qui in poi ogni archivio ci scrive.
- Data.AuctionDatabase.OnLog += m => TextLogService.App("INFO", m);
+ // I due database si aprono per primi: da qui in poi ogni archivio e ogni
+ // registro ci scrive. Quello d'esercizio per primo, perché il registro
+ // applicativo sta lì dentro.
+ Data.SqliteDatabase.OnLog += m => TextLogService.App("INFO", m);
try
{
+ var op = Data.OperationalDatabase.Instance;
+ TextLogService.SessionStarted(AppInfo.Version);
+ TextLogService.App("INFO", $"[DATABASE] Esercizio: {op.Path} ({op.SizeBytes / 1024:N0} KB)");
+
var db = Data.AuctionDatabase.Instance;
- TextLogService.App("INFO", $"[DATABASE] {db.Path} (SQLite {Data.AuctionDatabase.LibraryVersion}, {db.SizeBytes / 1024:N0} KB)");
- db.Enqueue("INSERT INTO sessions(started_at, app_version) VALUES(?1, ?2)", Data.AuctionDatabase.Now(), AppInfo.Version);
+ TextLogService.App("INFO", $"[DATABASE] Osservazioni: {db.Path} (SQLite {Data.SqliteDatabase.LibraryVersion}, {db.SizeBytes / 1024:N0} KB)");
+ db.Enqueue("INSERT INTO sessions(started_at, app_version) VALUES(?1, ?2)", Data.SqliteDatabase.Now(), AppInfo.Version);
}
catch (Exception ex)
{
TextLogService.App("ERROR", $"[DATABASE] Apertura non riuscita: {ex.Message}");
}
+ TextLogService.PurgeOldLogs();
+
// Le righe di registro delle aste confluiscono nel database.
Data.AuctionRecorder.CaptureAuctionLogs();
@@ -85,13 +83,13 @@ namespace AutoBidder
// Quello che è ancora in coda va sul disco adesso: un registro che perde
// proprio le ultime righe è inutile esattamente quando serve di più.
TextLogService.SessionEnded();
- FileLogWriter.DisposeAll();
try
{
var db = Data.AuctionDatabase.Instance;
- db.Enqueue("UPDATE sessions SET ended_at = ?1 WHERE session_id = (SELECT MAX(session_id) FROM sessions)", Data.AuctionDatabase.Now());
+ db.Enqueue("UPDATE sessions SET ended_at = ?1 WHERE session_id = (SELECT MAX(session_id) FROM sessions)", Data.SqliteDatabase.Now());
Data.AuctionDatabase.CloseInstance();
+ Data.OperationalDatabase.CloseInstance();
}
catch { /* all'uscita non c'è più nessuno a cui dirlo */ }
diff --git a/Mimante/Controls/AuctionMonitorControl.xaml b/Mimante/Controls/AuctionMonitorControl.xaml
index ed86b24..07b63ae 100644
--- a/Mimante/Controls/AuctionMonitorControl.xaml
+++ b/Mimante/Controls/AuctionMonitorControl.xaml
@@ -66,17 +66,10 @@
-
-
-
-
-
+
+
@@ -188,10 +181,6 @@
ToolTip="Rimuovi tutte le aste" Click="RemoveAllButton_Click">
-
@@ -491,11 +480,6 @@
Click="CopyAuctionUrlButton_Click">
-
diff --git a/Mimante/Controls/AuctionMonitorControl.xaml.cs b/Mimante/Controls/AuctionMonitorControl.xaml.cs
index 4f82bc6..80ee037 100644
--- a/Mimante/Controls/AuctionMonitorControl.xaml.cs
+++ b/Mimante/Controls/AuctionMonitorControl.xaml.cs
@@ -67,18 +67,18 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(StopClickedEvent, this));
}
- private void KillSwitchToggle_Changed(object sender, RoutedEventArgs e)
+ private void LearningToggle_Changed(object sender, RoutedEventArgs e)
{
- RaiseEvent(new RoutedEventArgs(KillSwitchChangedEvent, this));
+ RaiseEvent(new RoutedEventArgs(LearningToggledEvent, this));
}
- public static readonly RoutedEvent KillSwitchChangedEvent = EventManager.RegisterRoutedEvent(
- "KillSwitchChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
+ public static readonly RoutedEvent LearningToggledEvent = EventManager.RegisterRoutedEvent(
+ "LearningToggled", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
- public event RoutedEventHandler KillSwitchChanged
+ public event RoutedEventHandler LearningToggled
{
- add { AddHandler(KillSwitchChangedEvent, value); }
- remove { RemoveHandler(KillSwitchChangedEvent, value); }
+ add { AddHandler(LearningToggledEvent, value); }
+ remove { RemoveHandler(LearningToggledEvent, value); }
}
private void AddUrlButton_Click(object sender, RoutedEventArgs e)
@@ -96,11 +96,6 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(RemoveAllClickedEvent, this));
}
- private void ExportButton_Click(object sender, RoutedEventArgs e)
- {
- RaiseEvent(new RoutedEventArgs(ExportClickedEvent, this));
- }
-
private void RemoveFinishedButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(RemoveFinishedClickedEvent, this));
@@ -337,10 +332,6 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(OpenAuctionExternalClickedEvent, this));
}
- private void ExportAuctionButton_Click(object sender, RoutedEventArgs e)
- {
- RaiseEvent(new RoutedEventArgs(ExportAuctionClickedEvent, this));
- }
private void RefreshProductInfoButton_Click(object sender, RoutedEventArgs e)
{
@@ -410,9 +401,6 @@ namespace AutoBidder.Controls
public static readonly RoutedEvent RemoveAllClickedEvent = EventManager.RegisterRoutedEvent(
"RemoveAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
- public static readonly RoutedEvent ExportClickedEvent = EventManager.RegisterRoutedEvent(
- "ExportClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
-
public static readonly RoutedEvent AuctionSelectionChangedEvent = EventManager.RegisterRoutedEvent(
"AuctionSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
@@ -449,8 +437,6 @@ namespace AutoBidder.Controls
public static readonly RoutedEvent OpenAuctionExternalClickedEvent = EventManager.RegisterRoutedEvent(
"OpenAuctionExternalClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
- public static readonly RoutedEvent ExportAuctionClickedEvent = EventManager.RegisterRoutedEvent(
- "ExportAuctionClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent RefreshProductInfoClickedEvent = EventManager.RegisterRoutedEvent(
"RefreshProductInfoClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
@@ -507,12 +493,6 @@ namespace AutoBidder.Controls
remove { RemoveHandler(RemoveFinishedClickedEvent, value); }
}
- public event RoutedEventHandler ExportClicked
- {
- add { AddHandler(ExportClickedEvent, value); }
- remove { RemoveHandler(ExportClickedEvent, value); }
- }
-
public event RoutedEventHandler AuctionSelectionChanged
{
add { AddHandler(AuctionSelectionChangedEvent, value); }
@@ -597,12 +577,6 @@ namespace AutoBidder.Controls
remove { RemoveHandler(OpenAuctionExternalClickedEvent, value); }
}
- public event RoutedEventHandler ExportAuctionClicked
- {
- add { AddHandler(ExportAuctionClickedEvent, value); }
- remove { RemoveHandler(ExportAuctionClickedEvent, value); }
- }
-
public event RoutedEventHandler RefreshProductInfoClicked
{
add { AddHandler(RefreshProductInfoClickedEvent, value); }
diff --git a/Mimante/Controls/ExportControl.xaml b/Mimante/Controls/ExportControl.xaml
deleted file mode 100644
index 297bd18..0000000
--- a/Mimante/Controls/ExportControl.xaml
+++ /dev/null
@@ -1,248 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Mimante/Controls/ExportControl.xaml.cs b/Mimante/Controls/ExportControl.xaml.cs
deleted file mode 100644
index aa9d17b..0000000
--- a/Mimante/Controls/ExportControl.xaml.cs
+++ /dev/null
@@ -1,204 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Windows;
-using System.Windows.Controls;
-using AutoBidder.Utilities;
-
-namespace AutoBidder.Controls
-{
- ///
- /// Scheda Esporta: sceglie quali aste finiscono in un unico file e lo scrive.
- ///
- /// Come gli altri pannelli non conosce gli archivi: costruisce un
- /// e lascia il lavoro a
- /// . Qui dentro c'è solo la traduzione fra i campi
- /// dell'interfaccia e i criteri — e il conteggio in tempo reale, che serve a non
- /// scoprire dopo un minuto di scrittura che il filtro non selezionava nulla.
- ///
- public partial class ExportControl : UserControl
- {
- private string? _lastExportPath;
-
- public ExportControl()
- {
- InitializeComponent();
- Loaded += (_, _) => RefreshPreview();
- }
-
- /// I criteri come sono impostati adesso.
- public AuctionExportFilter BuildFilter() => new()
- {
- From = FromDatePicker.SelectedDate,
- To = ToDatePicker.SelectedDate,
- NameContains = string.IsNullOrWhiteSpace(NameFilterTextBox.Text) ? null : NameFilterTextBox.Text.Trim(),
-
- IncludeWon = IncludeWonCheckBox.IsChecked == true,
- IncludeLost = IncludeLostCheckBox.IsChecked == true,
- IncludeClosed = IncludeClosedCheckBox.IsChecked == true,
-
- OnlyComplete = OnlyCompleteCheckBox.IsChecked == true,
- OnlyWithMyBids = OnlyWithMyBidsCheckBox.IsChecked == true,
- OnlyWithDossier = OnlyWithDossierCheckBox.IsChecked == true,
-
- MinFinalPrice = ParseNumber(MinPriceTextBox.Text),
- MaxFinalPrice = ParseNumber(MaxPriceTextBox.Text),
- MaxAuctions = (int)(ParseNumber(MaxAuctionsTextBox.Text) ?? 0),
-
- IncludeEvents = IncludeEventsCheckBox.IsChecked == true
- };
-
- ///
- /// Ricalcola quante aste corrispondono. Si fa a ogni modifica dei filtri: sapere
- /// prima che il risultato è vuoto — o che sono duemila — evita di scoprirlo a file
- /// scritto.
- ///
- public void RefreshPreview()
- {
- try
- {
- var selected = AuctionExporter.Select(BuildFilter());
-
- MatchCountText.Text = selected.Count == 1 ? "1 asta" : $"{selected.Count} aste";
-
- var complete = selected.Count(r => r.IsComplete);
- CompleteCountText.Text = $"{complete} complete";
-
- ExportButton.IsEnabled = selected.Count > 0;
- }
- catch (Exception ex)
- {
- MatchCountText.Text = "—";
- CompleteCountText.Text = ex.Message;
- }
- }
-
- /// Mostra l'esito dell'esportazione appena fatta.
- public void ShowResult(AuctionExportResult result)
- {
- if (result.Success)
- {
- _lastExportPath = result.Path;
-
- var size = result.Bytes >= 1024 * 1024
- ? $"{result.Bytes / 1024.0 / 1024.0:F1} MB"
- : $"{Math.Max(1, result.Bytes / 1024)} kB";
-
- ResultText.Text = $"{result.AuctionCount} aste ({result.WithEvents} con eventi), {size} — {result.Path}";
- ResultText.SetResourceReference(ForegroundProperty, "Brush.Text");
- OpenLastFileButton.Visibility = Visibility.Visible;
- }
- else
- {
- ResultText.Text = result.Message;
- ResultText.SetResourceReference(ForegroundProperty, "Brush.Warning");
- OpenLastFileButton.Visibility = Visibility.Collapsed;
- }
- }
-
- /// Percorso dell'ultimo file scritto, per il pulsante "Apri il file".
- public string? LastExportPath => _lastExportPath;
-
- // ── Eventi dell'interfaccia ──────────────────────────────────────
-
- private void PreviewButton_Click(object sender, RoutedEventArgs e) => RefreshPreview();
-
- private void Filter_Changed(object sender, RoutedEventArgs e) => RefreshPreview();
-
- private void Last7Days_Click(object sender, RoutedEventArgs e) => SetPeriod(7);
-
- private void Last30Days_Click(object sender, RoutedEventArgs e) => SetPeriod(30);
-
- private void SetPeriod(int days)
- {
- FromDatePicker.SelectedDate = DateTime.Today.AddDays(-days);
- ToDatePicker.SelectedDate = DateTime.Today;
- RefreshPreview();
- }
-
- private void ResetFiltersButton_Click(object sender, RoutedEventArgs e)
- {
- FromDatePicker.SelectedDate = null;
- ToDatePicker.SelectedDate = null;
- NameFilterTextBox.Text = "";
- MinPriceTextBox.Text = "";
- MaxPriceTextBox.Text = "";
- MaxAuctionsTextBox.Text = "0";
-
- IncludeWonCheckBox.IsChecked = true;
- IncludeLostCheckBox.IsChecked = true;
- IncludeClosedCheckBox.IsChecked = true;
- OnlyCompleteCheckBox.IsChecked = false;
- OnlyWithMyBidsCheckBox.IsChecked = false;
- OnlyWithDossierCheckBox.IsChecked = false;
- IncludeEventsCheckBox.IsChecked = true;
-
- RefreshPreview();
- }
-
- private void ExportButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(ExportRequestedEvent, this));
-
- private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(OpenExportFolderClickedEvent, this));
-
- private void ClearExportsButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(ClearExportsClickedEvent, this));
-
- private void OpenLastFileButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(OpenLastFileClickedEvent, this));
-
- ///
- /// Legge un numero accettando sia la virgola sia il punto: chi scrive "12,50" e chi
- /// scrive "12.50" intende la stessa cosa, e rifiutarne uno dei due sarebbe pedanteria.
- ///
- private static double? ParseNumber(string? text)
- {
- if (string.IsNullOrWhiteSpace(text)) return null;
-
- var clean = text.Trim().Replace(',', '.');
-
- return double.TryParse(clean, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)
- ? value
- : null;
- }
-
- // ── Routed events ────────────────────────────────────────────────
-
- public static readonly RoutedEvent ExportRequestedEvent = EventManager.RegisterRoutedEvent(
- "ExportRequested", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
-
- public static readonly RoutedEvent OpenExportFolderClickedEvent = EventManager.RegisterRoutedEvent(
- "OpenExportFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
-
- public static readonly RoutedEvent OpenLastFileClickedEvent = EventManager.RegisterRoutedEvent(
- "OpenLastFileClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
-
- public static readonly RoutedEvent ClearExportsClickedEvent = EventManager.RegisterRoutedEvent(
- "ClearExportsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
-
- public event RoutedEventHandler ClearExportsClicked
- {
- add { AddHandler(ClearExportsClickedEvent, value); }
- remove { RemoveHandler(ClearExportsClickedEvent, value); }
- }
-
- public event RoutedEventHandler ExportRequested
- {
- add { AddHandler(ExportRequestedEvent, value); }
- remove { RemoveHandler(ExportRequestedEvent, value); }
- }
-
- public event RoutedEventHandler OpenExportFolderClicked
- {
- add { AddHandler(OpenExportFolderClickedEvent, value); }
- remove { RemoveHandler(OpenExportFolderClickedEvent, value); }
- }
-
- public event RoutedEventHandler OpenLastFileClicked
- {
- add { AddHandler(OpenLastFileClickedEvent, value); }
- remove { RemoveHandler(OpenLastFileClickedEvent, value); }
- }
- }
-}
diff --git a/Mimante/Controls/LearningControl.xaml b/Mimante/Controls/LearningControl.xaml
index 35e8b9a..4c1ab99 100644
--- a/Mimante/Controls/LearningControl.xaml
+++ b/Mimante/Controls/LearningControl.xaml
@@ -71,31 +71,28 @@
+
-
-
-
-
+
+
+
+
+
+ ToolTip="Ricomincia da capo.
Butta modello, profilo, sfidante, bandit e latenza appresa, e ristudia tutte le aste chiuse del database. Non tocca lo storico né i prodotti. Chiede conferma."
+ Click="RetrainButton_Click">
+
+
@@ -144,7 +141,7 @@
-
+
@@ -209,7 +206,7 @@
+ Help="Il rapporto dell'ultima valutazione sulle aste registrate in questo database. Le misure che contano: il sollevamento (quanto più spesso le puntate a probabilità alta erano davvero finali, rispetto al caso), la calibrazione per fasce (la probabilità prevista deve somigliare a quella osservata), e cosa il cancello avrebbe fatto sulle tue puntate vere."/>
Refresh();
- private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- var folder = Path.Combine(AppPaths.StatsFolder, "Apprendimento");
- Directory.CreateDirectory(folder);
- Process.Start(new ProcessStartInfo { FileName = folder, UseShellExecute = true });
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Cartella non apribile: {ex.Message}", "Apprendimento",
- MessageBoxButton.OK, MessageBoxImage.Warning);
- }
- }
-
private async void EvaluateButton_Click(object sender, RoutedEventArgs e)
{
if (_evaluation != null)
@@ -317,12 +321,14 @@ namespace AutoBidder.Controls
}
_evaluation = new CancellationTokenSource();
- EvaluateButton.Content = "Annulla";
- EvaluationBox.Text = "Valutazione in corso: lettura dei dossier…";
+ // Il pulsante è solo icona: mentre gira diventa una X, e il tooltip lo dice.
+ EvaluateGlyph.Text = "\uE711";
+ EvaluateButton.ToolTip = "Annulla la valutazione in corso.";
+ EvaluationBox.Text = "Valutazione in corso: lettura delle aste…";
try
{
- // Al massimo tremila dossier, i più recenti: bastano a giudicare, e la
+ // Al massimo tremila aste, le più recenti: bastano a giudicare, e la
// lettura di tutto l'archivio in sottofondo mentre le aste corrono non serve.
var report = await LearningService.EvaluateAsync(3000,
msg => Dispatcher.BeginInvoke(() => EvaluationBox.Text = "Valutazione in corso: " + msg),
@@ -341,15 +347,16 @@ namespace AutoBidder.Controls
finally
{
_evaluation = null;
- EvaluateButton.Content = "Valuta ora";
+ EvaluateGlyph.Text = "\uE9F5";
+ EvaluateButton.ToolTip = "Valuta ora.\nAddestra sulle aste più vecchie del database e giudica sulle più recenti, mai viste. Poi la prova prequenziale: ogni asta prima prevista e poi appresa, come fa il motore. Qualche minuto in sottofondo; il rapporto compare nella scheda «Valutazione».";
}
}
private async void RetrainButton_Click(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(
- "Butto modello, profilo ed elenco dei dossier letti, e ricomincio a studiare tutto l'archivio.\n\n" +
- "I dossier e lo storico non vengono toccati. Ci vorranno un paio di minuti in sottofondo.\n\nProcedo?",
+ "Butto modello, profilo, sfidante, bandit e contatore delle aste studiate, e ricomincio a studiare tutte le aste chiuse del database.\n\n" +
+ "Lo storico e i prodotti non vengono toccati. Ci vorranno un paio di minuti in sottofondo.\n\nProcedo?",
"Ricomincia da capo", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
diff --git a/Mimante/Controls/ProductsControl.xaml b/Mimante/Controls/ProductsControl.xaml
index afcbc03..caae560 100644
--- a/Mimante/Controls/ProductsControl.xaml
+++ b/Mimante/Controls/ProductsControl.xaml
@@ -155,7 +155,7 @@
-
+
-
-
+
-
+
+
-
+
-
+
+
-
+
+
+
+
diff --git a/Mimante/Controls/ProductsControl.xaml.cs b/Mimante/Controls/ProductsControl.xaml.cs
index 8df7d8f..e493803 100644
--- a/Mimante/Controls/ProductsControl.xaml.cs
+++ b/Mimante/Controls/ProductsControl.xaml.cs
@@ -135,6 +135,15 @@ namespace AutoBidder.Controls
private void ClearButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ClearClickedEvent, this));
+ private void ClearAllButton_Click(object sender, RoutedEventArgs e)
+ => RaiseEvent(new RoutedEventArgs(ClearAllClickedEvent, this));
+
+ private void UnwatchAllButton_Click(object sender, RoutedEventArgs e)
+ => RaiseEvent(new RoutedEventArgs(UnwatchAllClickedEvent, this));
+
+ private void RemoveUnwatchedButton_Click(object sender, RoutedEventArgs e)
+ => RaiseEvent(new RoutedEventArgs(RemoveUnwatchedClickedEvent, this));
+
private void ApplySuggestedButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ApplySuggestedClickedEvent, this));
@@ -158,6 +167,15 @@ namespace AutoBidder.Controls
public static readonly RoutedEvent ClearClickedEvent = EventManager.RegisterRoutedEvent(
"ClearClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
+ public static readonly RoutedEvent ClearAllClickedEvent = EventManager.RegisterRoutedEvent(
+ "ClearAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
+
+ public static readonly RoutedEvent UnwatchAllClickedEvent = EventManager.RegisterRoutedEvent(
+ "UnwatchAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
+
+ public static readonly RoutedEvent RemoveUnwatchedClickedEvent = EventManager.RegisterRoutedEvent(
+ "RemoveUnwatchedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
+
public static readonly RoutedEvent ApplySuggestedClickedEvent = EventManager.RegisterRoutedEvent(
"ApplySuggestedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
@@ -200,6 +218,24 @@ namespace AutoBidder.Controls
remove { RemoveHandler(ClearClickedEvent, value); }
}
+ public event RoutedEventHandler ClearAllClicked
+ {
+ add { AddHandler(ClearAllClickedEvent, value); }
+ remove { RemoveHandler(ClearAllClickedEvent, value); }
+ }
+
+ public event RoutedEventHandler UnwatchAllClicked
+ {
+ add { AddHandler(UnwatchAllClickedEvent, value); }
+ remove { RemoveHandler(UnwatchAllClickedEvent, value); }
+ }
+
+ public event RoutedEventHandler RemoveUnwatchedClicked
+ {
+ add { AddHandler(RemoveUnwatchedClickedEvent, value); }
+ remove { RemoveHandler(RemoveUnwatchedClickedEvent, value); }
+ }
+
public event RoutedEventHandler ApplySuggestedClicked
{
add { AddHandler(ApplySuggestedClickedEvent, value); }
diff --git a/Mimante/Controls/SettingsControl.xaml b/Mimante/Controls/SettingsControl.xaml
index 5e4fcf4..9da8a5d 100644
--- a/Mimante/Controls/SettingsControl.xaml
+++ b/Mimante/Controls/SettingsControl.xaml
@@ -53,9 +53,9 @@
-
+
-
+
@@ -365,7 +365,7 @@ L'elenco dei prodotti è nella scheda Prodotti: lì ogni articolo ha la stellina
+ ToolTip="Aggiungi da solo soltanto le aste su cui nessuno ha ancora puntato, per poterle seguire dal primo istante all'ultimo.
Un'asta presa a metà si può seguire, ma la sua storia è già scritta per buona parte: non si sa quanto è durata davvero, chi c'era prima, come è salito il prezzo. Le statistiche che ne escono sono monche e la registrazione racconta solo la seconda metà.
Il prezzo da pagare è che si perdono le occasioni già in corso."/>
@@ -380,59 +380,6 @@ L'elenco dei prodotti è nella scheda Prodotti: lì ogni articolo ha la stellina
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -491,96 +438,29 @@ Dalla rigiocata di 2.095 aste concluse: con anticipo sotto il secondo serve in m
-
+
-
+ Help="Tutto ciò che l'applicazione registra sta in due file SQLite in questa cartella: le osservazioni (aste, puntate, interrogazioni, decisioni, misure) e l'esercizio (aste nel monitor, prodotti seguiti, modelli appresi, registri). Le impostazioni e la login restano nelle loro cartelle.
Se cambi cartella, al salvataggio ti chiedo se spostare i database esistenti; in ogni caso l'applicazione si riavvia. Mai una cartella sincronizzata (OneDrive, Dropbox): un database aperto sotto sincronizzazione si corrompe."/>
-
+
-
+
+ Click="BrowseDatabaseFolderButton_Click"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -588,17 +468,41 @@ Dalla rigiocata di 2.095 aste concluse: con anticipo sotto il secondo serve in m
+
+
+
+
+
+
+
+
+
+
+
+
-
+ Help="I registri si scrivono mentre l'applicazione lavora, nel database d'esercizio: quello che vedi nelle schede è già sul disco. Servono a rileggere cosa è successo mentre non guardavi. Si esportano in testo con il pulsante qui sotto e scadono dopo il numero di giorni indicato."/>
+ Content="Registro applicativo nel database"/>
+ Content="Registro del riscatto puntate nel database"/>
+
+
@@ -699,7 +610,7 @@ Se il valore è maggiore di zero, il sistema non punta quando le puntate residue
+ Help="I paletti duri che nessuna strategia può scavalcare. Ogni puntata dal vivo passa da qui prima di partire.
Il tetto di spesa del giorno ferma le puntate per oggi e domani si riparte. Lo stop-loss (perdita netta del giorno) e il drawdown (discesa dal picco del saldo cumulato) mettono invece il sistema in HALT: resta fermo anche dopo un riavvio, finché non lo togli tu da qui. Le vincite si contano al valore reale del prodotto (colonna «Valore reale» nella scheda Prodotti), meno prezzo, fee e spedizione.
La copertura «Compralo Ora» presume che, perdendo, si compri al listino recuperando le puntate come credito: la perdita si limita a listino − valore reale. Per i buoni al nominale copre tutto dal primo centesimo, e il modello punterà più spesso. Se non intendi esercitarla, spegnila."/>
@@ -743,12 +654,8 @@ Se il valore è maggiore di zero, il sistema non punta quando le puntate residue
-
+
-
-
-
diff --git a/Mimante/Controls/SettingsControl.xaml.cs b/Mimante/Controls/SettingsControl.xaml.cs
index 796e668..d49fb9b 100644
--- a/Mimante/Controls/SettingsControl.xaml.cs
+++ b/Mimante/Controls/SettingsControl.xaml.cs
@@ -62,46 +62,22 @@ namespace AutoBidder.Controls
// ===== ANTICIPO, CARTELLE, ESPORTAZIONE =====
- private void ComputeBidLeadAdviceButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(ComputeBidLeadAdviceClickedEvent, this));
-
- private void ClearBidLeadStatsButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(ClearBidLeadStatsClickedEvent, this));
-
- private void BrowseDataFolderButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(BrowseDataFolderClickedEvent, this));
-
- private void BrowseStatsFolderButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(BrowseStatsFolderClickedEvent, this));
-
- private void OpenDataFolderButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(OpenDataFolderClickedEvent, this));
-
- private void OpenStatsFolderSettingsButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(OpenStatsFolderClickedEvent, this));
-
- private void BrowseLogFolderButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(BrowseLogFolderClickedEvent, this));
-
- private void OpenLogFolderButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(OpenLogFolderClickedEvent, this));
-
- private void BrowseDatabaseFileButton_Click(object sender, RoutedEventArgs e)
- => RaiseEvent(new RoutedEventArgs(BrowseDatabaseFileClickedEvent, this));
+ private void BrowseDatabaseFolderButton_Click(object sender, RoutedEventArgs e)
+ => RaiseEvent(new RoutedEventArgs(BrowseDatabaseFolderClickedEvent, this));
private void OpenDatabaseFolderButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenDatabaseFolderClickedEvent, this));
- public static readonly RoutedEvent BrowseDatabaseFileClickedEvent = EventManager.RegisterRoutedEvent(
- "BrowseDatabaseFileClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
+ public static readonly RoutedEvent BrowseDatabaseFolderClickedEvent = EventManager.RegisterRoutedEvent(
+ "BrowseDatabaseFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent OpenDatabaseFolderClickedEvent = EventManager.RegisterRoutedEvent(
"OpenDatabaseFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
- public event RoutedEventHandler BrowseDatabaseFileClicked
+ public event RoutedEventHandler BrowseDatabaseFolderClicked
{
- add { AddHandler(BrowseDatabaseFileClickedEvent, value); }
- remove { RemoveHandler(BrowseDatabaseFileClickedEvent, value); }
+ add { AddHandler(BrowseDatabaseFolderClickedEvent, value); }
+ remove { RemoveHandler(BrowseDatabaseFolderClickedEvent, value); }
}
public event RoutedEventHandler OpenDatabaseFolderClicked
@@ -112,14 +88,41 @@ namespace AutoBidder.Controls
// ── Gestione del rischio: kill-switch e HALT si applicano subito ──
- private bool _riskSyncing;
- private void KillSwitchCheckBox_Changed(object sender, RoutedEventArgs e)
+ private void WipeAllDataButton_Click(object sender, RoutedEventArgs e)
+ => RaiseEvent(new RoutedEventArgs(WipeAllDataClickedEvent, this));
+
+ private void ResetSettingsButton_Click(object sender, RoutedEventArgs e)
+ => RaiseEvent(new RoutedEventArgs(ResetSettingsClickedEvent, this));
+
+ private void ExportAppLogButton_Click(object sender, RoutedEventArgs e)
+ => RaiseEvent(new RoutedEventArgs(ExportAppLogClickedEvent, this));
+
+ public static readonly RoutedEvent WipeAllDataClickedEvent = EventManager.RegisterRoutedEvent(
+ "WipeAllDataClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
+
+ public static readonly RoutedEvent ResetSettingsClickedEvent = EventManager.RegisterRoutedEvent(
+ "ResetSettingsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
+
+ public static readonly RoutedEvent ExportAppLogClickedEvent = EventManager.RegisterRoutedEvent(
+ "ExportAppLogClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
+
+ public event RoutedEventHandler WipeAllDataClicked
{
- if (_riskSyncing) return;
- Services.RiskManager.SetKillSwitch(KillSwitchCheckBox.IsChecked == true);
- RefreshRiskStatus();
- RaiseEvent(new RoutedEventArgs(KillSwitchChangedEvent, this));
+ add { AddHandler(WipeAllDataClickedEvent, value); }
+ remove { RemoveHandler(WipeAllDataClickedEvent, value); }
+ }
+
+ public event RoutedEventHandler ResetSettingsClicked
+ {
+ add { AddHandler(ResetSettingsClickedEvent, value); }
+ remove { RemoveHandler(ResetSettingsClickedEvent, value); }
+ }
+
+ public event RoutedEventHandler ExportAppLogClicked
+ {
+ add { AddHandler(ExportAppLogClickedEvent, value); }
+ remove { RemoveHandler(ExportAppLogClickedEvent, value); }
}
private void ResumeHaltButton_Click(object sender, RoutedEventArgs e)
@@ -128,111 +131,24 @@ namespace AutoBidder.Controls
RefreshRiskStatus();
}
- /// Riallinea kill-switch e HALT a quello che dicono il file e il database.
+ /// Riallinea lo stato dell'HALT e la contabilità a quello che dice il database.
public void RefreshRiskStatus()
{
try
{
- _riskSyncing = true;
- KillSwitchCheckBox.IsChecked = Services.RiskManager.KillSwitchActive;
-
var halt = Services.RiskManager.HaltReason;
var s = Services.RiskManager.Snapshot();
var conto = $"Oggi: spesi {s.TodaySpent:F2} €, incassati {s.TodayGained:F2} € (netto {s.TodayNet:+0.00;-0.00} €). " +
$"Saldo cumulato {s.Cumulative:+0.00;-0.00} €, picco {s.Peak:F2} €, drawdown {s.Drawdown:F2} €.";
RiskStatusText.Text = string.IsNullOrEmpty(halt)
- ? (Services.RiskManager.KillSwitchActive ? "Kill-switch ATTIVO: nessuna puntata dal vivo.\n" : "Nessun HALT in corso.\n") + conto
+ ? "Nessun HALT in corso.\n" + conto
: $"HALT in corso: {halt}\n{conto}";
ResumeHaltButton.Visibility = string.IsNullOrEmpty(halt) ? Visibility.Collapsed : Visibility.Visible;
}
catch { }
- finally { _riskSyncing = false; }
- }
-
- public static readonly RoutedEvent KillSwitchChangedEvent = EventManager.RegisterRoutedEvent(
- "KillSwitchChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public event RoutedEventHandler KillSwitchChanged
- {
- add { AddHandler(KillSwitchChangedEvent, value); }
- remove { RemoveHandler(KillSwitchChangedEvent, value); }
- }
-
- /// Riepilogo delle misure sull'anticipo, mostrato nel riquadro informativo.
- public void SetBidLeadSummary(string text) => BidLeadSummaryText.Text = text;
-
- public static readonly RoutedEvent ComputeBidLeadAdviceClickedEvent = EventManager.RegisterRoutedEvent(
- "ComputeBidLeadAdviceClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public static readonly RoutedEvent ClearBidLeadStatsClickedEvent = EventManager.RegisterRoutedEvent(
- "ClearBidLeadStatsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public static readonly RoutedEvent BrowseDataFolderClickedEvent = EventManager.RegisterRoutedEvent(
- "BrowseDataFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public static readonly RoutedEvent BrowseStatsFolderClickedEvent = EventManager.RegisterRoutedEvent(
- "BrowseStatsFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public static readonly RoutedEvent OpenDataFolderClickedEvent = EventManager.RegisterRoutedEvent(
- "OpenDataFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public static readonly RoutedEvent OpenStatsFolderClickedEvent = EventManager.RegisterRoutedEvent(
- "OpenStatsFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public static readonly RoutedEvent BrowseLogFolderClickedEvent = EventManager.RegisterRoutedEvent(
- "BrowseLogFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public static readonly RoutedEvent OpenLogFolderClickedEvent = EventManager.RegisterRoutedEvent(
- "OpenLogFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
-
- public event RoutedEventHandler ComputeBidLeadAdviceClicked
- {
- add { AddHandler(ComputeBidLeadAdviceClickedEvent, value); }
- remove { RemoveHandler(ComputeBidLeadAdviceClickedEvent, value); }
- }
-
- public event RoutedEventHandler ClearBidLeadStatsClicked
- {
- add { AddHandler(ClearBidLeadStatsClickedEvent, value); }
- remove { RemoveHandler(ClearBidLeadStatsClickedEvent, value); }
- }
-
- public event RoutedEventHandler BrowseDataFolderClicked
- {
- add { AddHandler(BrowseDataFolderClickedEvent, value); }
- remove { RemoveHandler(BrowseDataFolderClickedEvent, value); }
- }
-
- public event RoutedEventHandler BrowseStatsFolderClicked
- {
- add { AddHandler(BrowseStatsFolderClickedEvent, value); }
- remove { RemoveHandler(BrowseStatsFolderClickedEvent, value); }
- }
-
- public event RoutedEventHandler OpenDataFolderClicked
- {
- add { AddHandler(OpenDataFolderClickedEvent, value); }
- remove { RemoveHandler(OpenDataFolderClickedEvent, value); }
- }
-
- public event RoutedEventHandler OpenStatsFolderClicked
- {
- add { AddHandler(OpenStatsFolderClickedEvent, value); }
- remove { RemoveHandler(OpenStatsFolderClickedEvent, value); }
- }
-
- public event RoutedEventHandler BrowseLogFolderClicked
- {
- add { AddHandler(BrowseLogFolderClickedEvent, value); }
- remove { RemoveHandler(BrowseLogFolderClickedEvent, value); }
- }
-
- public event RoutedEventHandler OpenLogFolderClicked
- {
- add { AddHandler(OpenLogFolderClickedEvent, value); }
- remove { RemoveHandler(OpenLogFolderClickedEvent, value); }
+ finally { }
}
// ===== AGGIUNTA AUTOMATICA =====
diff --git a/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs b/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs
index a2b491a..776c477 100644
--- a/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs
+++ b/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs
@@ -84,10 +84,7 @@ namespace AutoBidder
Settings.CatalogMaxAuctionsTextBox.Text = settings.CatalogMaxAuctions.ToString();
Settings.CatalogAutoRefreshCheckBox.IsChecked = settings.CatalogAutoRefresh;
- // Anticipo, aste programmate, notifiche, cartelle
- Settings.BidLeadTrackingCheckBox.IsChecked = settings.BidLeadTrackingEnabled;
- Settings.BidLeadMinSamplesTextBox.Text = settings.BidLeadMinSamples.ToString();
-
+ // Aste programmate, notifiche, cartelle
Settings.ScheduledBackoffCheckBox.IsChecked = settings.ScheduledAuctionBackoffEnabled;
Settings.ScheduledPollFarTextBox.Text = settings.ScheduledPollFarSeconds.ToString();
Settings.ScheduledPollMidTextBox.Text = settings.ScheduledPollMidSeconds.ToString();
@@ -112,12 +109,12 @@ namespace AutoBidder
Settings.AutoRemoveKeepWonCheckBox.IsChecked = settings.AutoRemoveKeepWon;
Settings.AutoRemoveKeepUnclearCheckBox.IsChecked = settings.AutoRemoveKeepUnclear;
- Settings.WriteAppLogCheckBox.IsChecked = settings.WriteAppLogFile;
- Settings.WriteFreeBidsLogCheckBox.IsChecked = settings.WriteFreeBidsLogFile;
- Settings.WriteDossiersCheckBox.IsChecked = settings.WriteAuctionDossiers;
- Settings.RawPollsCheckBox.IsChecked = settings.DossierIncludeRawPolls;
+ Settings.WriteAppLogCheckBox.IsChecked = settings.WriteAppLog;
+ Settings.WriteFreeBidsLogCheckBox.IsChecked = settings.WriteFreeBidsLog;
+ Settings.WriteDossiersCheckBox.IsChecked = settings.RecordAuctions;
+ Settings.RawPollsCheckBox.IsChecked = settings.RecordPolls;
Settings.LogRetentionTextBox.Text = settings.LogRetentionDays.ToString();
- Settings.DatabaseFileTextBox.Text = AppPaths.DatabaseFile;
+ Settings.DatabaseFolderTextBox.Text = AppPaths.DatabaseFolder;
Settings.BankrollEnabledCheckBox.IsChecked = settings.BankrollManagerEnabled;
Settings.MaxBidsPerAuctionTextBox.Text = settings.MaxBidsPerAuction.ToString();
@@ -130,8 +127,6 @@ namespace AutoBidder
Settings.HedgeCheckBox.IsChecked = settings.BuyNowHedgeEnabled;
Settings.RefreshRiskStatus();
- RefreshBidLeadSummary();
-
// Aggiorna indicatore visivo
UpdateMinBidsIndicator(settings.MinimumRemainingBids);
@@ -403,11 +398,6 @@ namespace AutoBidder
settings.CatalogCacheSeconds = ReadBounded(Settings.CatalogCacheSecondsTextBox.Text,
settings.CatalogCacheSeconds, 0, 3600, "cache catalogo", " s");
- // === SEZIONE: Anticipo puntata ===
- settings.BidLeadTrackingEnabled = Settings.BidLeadTrackingCheckBox.IsChecked ?? true;
- settings.BidLeadMinSamples = ReadBounded(Settings.BidLeadMinSamplesTextBox.Text,
- settings.BidLeadMinSamples, 5, 500, "puntate minime per il consiglio", "");
-
// === SEZIONE: Aste programmate ===
settings.ScheduledAuctionBackoffEnabled = Settings.ScheduledBackoffCheckBox.IsChecked ?? true;
settings.ScheduledPollFarSeconds = ReadBounded(Settings.ScheduledPollFarTextBox.Text,
@@ -422,15 +412,7 @@ namespace AutoBidder
settings.NotifyOnLoss = Settings.NotifyOnLossCheckBox.IsChecked ?? false;
// === SEZIONE: Cartelle dei dati ===
- var previousData = AppPaths.DataFolder;
- var previousStats = AppPaths.StatsFolder;
-
- // Il campo contiene il percorso risolto: se coincide con il predefinito si
- // salva vuoto, così spostando la cartella dati i registri la seguono invece
- // di restare inchiodati a un percorso che nessuno ha mai scelto davvero.
- settings.DataFolder = NormalizeFolderChoice(Settings.DataFolderTextBox.Text, AppPaths.DataFolder, settings.DataFolder);
- settings.StatsFolder = NormalizeFolderChoice(Settings.StatsFolderTextBox.Text, AppPaths.StatsFolder, settings.StatsFolder);
- settings.LogFolder = NormalizeFolderChoice(Settings.LogFolderTextBox.Text, AppPaths.LogFolder, settings.LogFolder);
+ var previousDatabaseFolder = AppPaths.DatabaseFolder;
settings.QuietHoursEnabled = Settings.QuietHoursCheckBox.IsChecked ?? true;
@@ -444,18 +426,15 @@ namespace AutoBidder
settings.AutoRemoveKeepWon = Settings.AutoRemoveKeepWonCheckBox.IsChecked ?? true;
settings.AutoRemoveKeepUnclear = Settings.AutoRemoveKeepUnclearCheckBox.IsChecked ?? true;
- settings.WriteAppLogFile = Settings.WriteAppLogCheckBox.IsChecked ?? true;
- settings.WriteFreeBidsLogFile = Settings.WriteFreeBidsLogCheckBox.IsChecked ?? true;
- settings.WriteAuctionDossiers = Settings.WriteDossiersCheckBox.IsChecked ?? true;
- settings.DossierIncludeRawPolls = Settings.RawPollsCheckBox.IsChecked ?? true;
+ settings.WriteAppLog = Settings.WriteAppLogCheckBox.IsChecked ?? true;
+ settings.WriteFreeBidsLog = Settings.WriteFreeBidsLogCheckBox.IsChecked ?? true;
+ settings.RecordAuctions = Settings.WriteDossiersCheckBox.IsChecked ?? true;
+ settings.RecordPolls = Settings.RawPollsCheckBox.IsChecked ?? true;
if (int.TryParse(Settings.LogRetentionTextBox.Text?.Trim(), out var retention) && retention >= 0)
settings.LogRetentionDays = retention;
- var previousDb = AppPaths.DatabaseFile;
- settings.DatabasePath = NormalizeFolderChoice(Settings.DatabaseFileTextBox.Text, AppPaths.DatabaseFile, settings.DatabasePath);
- if (!string.Equals(previousDb, settings.DatabasePath.Length > 0 ? settings.DatabasePath : AppPaths.DatabaseFile, StringComparison.OrdinalIgnoreCase))
- Log("[DATI] Percorso del database cambiato: vale dal prossimo avvio", LogLevel.Warning);
+ settings.DatabaseFolder = NormalizeFolderChoice(Settings.DatabaseFolderTextBox.Text, AppPaths.DatabaseFolder, settings.DatabaseFolder);
settings.BankrollManagerEnabled = Settings.BankrollEnabledCheckBox.IsChecked ?? true;
settings.MaxBidsPerAuction = ReadBounded(Settings.MaxBidsPerAuctionTextBox.Text, settings.MaxBidsPerAuction, 0, 100000, "puntate massime per asta", "");
@@ -469,7 +448,7 @@ namespace AutoBidder
Utilities.SettingsManager.Save(settings);
- ApplyDataFolderSettings(settings, previousData, previousStats);
+ ApplyDatabaseFolderSetting(settings, previousDatabaseFolder);
// Il limite di ritmo si applica a caldo, senza ricreare il trasporto.
_auctionMonitor.ApplyTransportSettings(settings);
diff --git a/Mimante/Core/MainWindow.ButtonHandlers.cs b/Mimante/Core/MainWindow.ButtonHandlers.cs
index ede4d3e..26b2e24 100644
--- a/Mimante/Core/MainWindow.ButtonHandlers.cs
+++ b/Mimante/Core/MainWindow.ButtonHandlers.cs
@@ -456,31 +456,6 @@ namespace AutoBidder
}
}
- private void ExportAuctionButton_Click(object sender, RoutedEventArgs e)
- {
- if (_selectedAuction == null)
- {
- MessageBox.Show("Seleziona un'asta dalla griglia", "Nessuna Selezione", MessageBoxButton.OK, MessageBoxImage.Information);
- return;
- }
-
- try
- {
- MessageBox.Show(
- $"Esportazione singola asta:\n\n{_selectedAuction.Name}\n(ID: {_selectedAuction.AuctionId})\n\nFunzionalit� in sviluppo.\nUsa 'Esporta' dalla toolbar per esportare tutte le aste.",
- "Export Asta",
- MessageBoxButton.OK,
- MessageBoxImage.Information);
-
- Log($"[INFO] Richiesto export singolo per asta: {_selectedAuction.Name} (funzionalit� in sviluppo)", LogLevel.Info);
- }
- catch (Exception ex)
- {
- Log($"[ERRORE] Export asta: {ex.Message}", LogLevel.Error);
- MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
private async void RefreshProductInfoButton_Click(object sender, RoutedEventArgs e)
{
try
diff --git a/Mimante/Core/MainWindow.ControlEvents.cs b/Mimante/Core/MainWindow.ControlEvents.cs
index 5c2a9bb..f94cb74 100644
--- a/Mimante/Core/MainWindow.ControlEvents.cs
+++ b/Mimante/Core/MainWindow.ControlEvents.cs
@@ -39,15 +39,6 @@ namespace AutoBidder
RefreshFreeBidsPanel();
}
- private void TabEsporta_Checked(object sender, RoutedEventArgs e)
- {
- ShowPanel(Export);
-
- // Il conteggio si rifà all'apertura: nel frattempo possono essersi concluse
- // altre aste, e un numero vecchio farebbe scegliere i filtri al buio.
- Export.RefreshPreview();
- }
-
private void TabDatiStatistici_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(StatisticsPanel);
@@ -80,7 +71,7 @@ namespace AutoBidder
{
// Prevent NullReferenceException during initialization
if (AuctionMonitor == null || Browser == null || StatisticsPanel == null ||
- Settings == null || FreeBids == null || Products == null || Export == null ||
+ Settings == null || FreeBids == null || Products == null ||
Learning == null)
return;
@@ -90,7 +81,6 @@ namespace AutoBidder
Products.Visibility = Visibility.Collapsed;
FreeBids.Visibility = Visibility.Collapsed;
StatisticsPanel.Visibility = Visibility.Collapsed;
- Export.Visibility = Visibility.Collapsed;
Settings.Visibility = Visibility.Collapsed;
Learning.Visibility = Visibility.Collapsed;
@@ -116,29 +106,6 @@ namespace AutoBidder
StopButton_Click(sender, e);
}
- private void AuctionMonitor_ExportClicked(object sender, RoutedEventArgs e)
- {
- // Chiama il metodo di export esistente
- try
- {
- // Esporta tutte le aste monitorate
- var auctions = _auctionMonitor.GetAuctions();
- if (auctions.Count == 0)
- {
- System.Windows.MessageBox.Show("Nessuna asta da esportare", "Export", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
- return;
- }
-
- // TODO: Implementare dialog export con scelta formato
- System.Windows.MessageBox.Show($"Export di {auctions.Count} aste.\n\nFunzionalit� in sviluppo.\nUsa le impostazioni nella scheda Impostazioni per configurare l'export.", "Export Aste", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
- Log($"[INFO] Richiesto export di {auctions.Count} aste (funzionalit� in sviluppo)", Utilities.LogLevel.Info);
- }
- catch (System.Exception ex)
- {
- Log($"[ERRORE] Export: {ex.Message}", Utilities.LogLevel.Error);
- }
- }
-
private void AuctionMonitor_AddUrlClicked(object sender, RoutedEventArgs e)
{
AddUrlButton_Click(sender, e);
@@ -207,11 +174,6 @@ namespace AutoBidder
OpenAuctionExternalButton_Click(sender, e);
}
- private void AuctionMonitor_ExportAuctionClicked(object sender, RoutedEventArgs e)
- {
- ExportAuctionButton_Click(sender, e);
- }
-
private void AuctionMonitor_ResetSettingsClicked(object sender, RoutedEventArgs e)
{
ResetSettingsButton_Click(sender, e);
diff --git a/Mimante/Core/MainWindow.DataSettings.cs b/Mimante/Core/MainWindow.DataSettings.cs
index 08fac0d..fee5f3d 100644
--- a/Mimante/Core/MainWindow.DataSettings.cs
+++ b/Mimante/Core/MainWindow.DataSettings.cs
@@ -2,121 +2,29 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Windows;
+using AutoBidder.Data;
using AutoBidder.Utilities;
namespace AutoBidder
{
///
- /// Cartelle dei dati, misura dell'anticipo ed esportazione dello storico.
+ /// La cartella dei database (con la migrazione quando cambia) e le esportazioni dello
+ /// storico, che chiedono sempre dove salvare.
///
public partial class MainWindow
{
- // ── Anticipo: misura e consiglio ─────────────────────────────────
+ // ── Cartella dei database ────────────────────────────────────────
- private void RefreshBidLeadSummary()
- {
- try
- {
- var s = BidLeadStats.Summarize();
-
- if (!s.HasData)
- {
- Settings.SetBidLeadSummary(
- "Nessuna puntata registrata finora. Le misure si raccolgono da sole a ogni puntata.");
- return;
- }
-
- Settings.SetBidLeadSummary(
- $"{s.Count} puntate misurate — {s.Successes} riuscite ({s.SuccessRate:F0}%), {s.TooLate} tardive.\n" +
- $"Arrivano in media {s.AverageActualMs:F0} ms prima della scadenza " +
- $"(mediana {s.MedianActualMs:F0} ms, caso peggiore {s.MinActualMs:F0} ms).\n" +
- $"Ping medio {s.AveragePingMs:F0} ms, anticipo impostato di norma {s.TypicalConfiguredMs} ms.");
- }
- catch { }
- }
-
- private void Settings_ComputeBidLeadAdviceClicked(object sender, RoutedEventArgs e)
- {
- try
- {
- var settings = SettingsManager.Load();
- var current = settings.DefaultBidBeforeDeadlineMs;
- var advice = BidLeadStats.Suggest(current, settings.BidLeadMinSamples);
-
- RefreshBidLeadSummary();
-
- if (!advice.HasAdvice)
- {
- MessageBox.Show(this, advice.Reason, "Anticipo puntata",
- MessageBoxButton.OK, MessageBoxImage.Information);
- return;
- }
-
- // Il consiglio non si applica mai da solo: cambia il parametro che decide
- // se una puntata arriva o no, e la decisione resta dell'utente.
- var answer = MessageBox.Show(this,
- $"{advice.Reason}\n\nVuoi impostare l'anticipo predefinito a {advice.SuggestedLeadMs} ms?\n\n" +
- "Le aste già nel monitor mantengono il proprio valore.",
- "Anticipo puntata", MessageBoxButton.YesNo, MessageBoxImage.Question);
-
- if (answer != MessageBoxResult.Yes) return;
-
- settings.DefaultBidBeforeDeadlineMs = advice.SuggestedLeadMs;
- SettingsManager.Save(settings);
-
- DefaultBidBeforeDeadlineMs.Text = advice.SuggestedLeadMs.ToString();
- Log($"[ANTICIPO] Anticipo predefinito portato a {advice.SuggestedLeadMs} ms", LogLevel.Success);
- }
- catch (Exception ex)
- {
- Log($"[ANTICIPO] {ex.Message}", LogLevel.Error);
- }
- }
-
- private void Settings_ClearBidLeadStatsClicked(object sender, RoutedEventArgs e)
- {
- var answer = MessageBox.Show(this,
- "Vuoi azzerare tutte le misure sull'anticipo?\n\n" +
- "Serviranno di nuovo diverse puntate prima di poter dare un consiglio.",
- "Misure anticipo", MessageBoxButton.YesNo, MessageBoxImage.Question);
-
- if (answer != MessageBoxResult.Yes) return;
-
- BidLeadStats.Clear();
- RefreshBidLeadSummary();
- Log("[ANTICIPO] Misure azzerate", LogLevel.Info);
- }
-
- // ── Cartelle dei dati ────────────────────────────────────────────
-
- ///
- /// Riempie i campi dei percorsi con quelli davvero in uso.
- ///
- /// I campi non restano mai vuoti: un campo vuoto costringeva a cercare altrove
- /// dove finiscono i file, ed è il motivo per cui sotto c'era una riga che ripeteva i
- /// percorsi. Scriverli direttamente nel campo toglie la ripetizione e rende il valore
- /// selezionabile e copiabile. Chi vuole tornare al predefinito lo svuota: al
- /// salvataggio si ricompila da sé.
- ///
+ /// Il campo mostra sempre il percorso in uso: selezionabile, copiabile.
private void RefreshDataFolderFields()
{
- try
- {
- Settings.DataFolderTextBox.Text = AppPaths.DataFolder;
- Settings.StatsFolderTextBox.Text = AppPaths.StatsFolder;
- Settings.LogFolderTextBox.Text = AppPaths.LogFolder;
- }
+ try { Settings.DatabaseFolderTextBox.Text = AppPaths.DatabaseFolder; }
catch { }
}
///
- /// Traduce quello che c'è nel campo in ciò che va salvato.
- ///
- /// Il campo mostra sempre un percorso pieno, ma nelle impostazioni "vuoto"
- /// ha un significato che non si vuole perdere: usa il predefinito. Se il
- /// testo coincide con il predefinito corrente si salva vuoto — così spostando la
- /// cartella dati le sottocartelle la seguono — e se il campo è stato svuotato a mano
- /// si torna al predefinito. Solo un percorso davvero diverso viene registrato.
+ /// Traduce quello che c'è nel campo in ciò che va salvato: vuoto = predefinito,
+ /// uguale al predefinito = vuoto, un percorso davvero diverso = quel percorso.
///
private static string NormalizeFolderChoice(string? typed, string resolvedDefault, string previouslySaved)
{
@@ -124,7 +32,6 @@ namespace AutoBidder
if (text.Length == 0) return "";
- // Uguale a ciò che è in uso, e in uso c'era il predefinito: resta predefinito.
if (string.Equals(text, resolvedDefault, StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrWhiteSpace(previouslySaved))
return "";
@@ -132,36 +39,17 @@ namespace AutoBidder
return text;
}
- private void Settings_BrowseDataFolderClicked(object sender, RoutedEventArgs e)
+ private void Settings_BrowseDatabaseFolderClicked(object sender, RoutedEventArgs e)
{
- var chosen = PickFolder("Scegli la cartella dei dati d'esercizio", AppPaths.DataFolder);
- if (chosen != null) Settings.DataFolderTextBox.Text = chosen;
+ var chosen = PickFolder("Scegli la cartella dei database (non una cartella sincronizzata)", AppPaths.DatabaseFolder);
+ if (chosen != null) Settings.DatabaseFolderTextBox.Text = chosen;
}
- private void Settings_BrowseStatsFolderClicked(object sender, RoutedEventArgs e)
- {
- var chosen = PickFolder("Scegli la cartella delle statistiche", AppPaths.StatsFolder);
- if (chosen != null) Settings.StatsFolderTextBox.Text = chosen;
- }
-
- private void Settings_BrowseLogFolderClicked(object sender, RoutedEventArgs e)
- {
- var chosen = PickFolder("Scegli la cartella dei registri", AppPaths.LogFolder);
- if (chosen != null) Settings.LogFolderTextBox.Text = chosen;
- }
-
- private void Settings_OpenDataFolderClicked(object sender, RoutedEventArgs e)
- => OpenFolder(AppPaths.DataFolder);
-
- private void Settings_OpenStatsFolderClicked(object sender, RoutedEventArgs e)
- => OpenFolder(AppPaths.StatsFolder);
-
- private void Settings_OpenLogFolderClicked(object sender, RoutedEventArgs e)
- => OpenFolder(AppPaths.LogFolder);
+ private void Settings_OpenDatabaseFolderClicked(object sender, RoutedEventArgs e)
+ => OpenFolder(AppPaths.DatabaseFolder);
///
- /// Selettore di cartella. Usa quello di WinForms perché WPF non ne offre uno:
- /// l'alternativa sarebbe una finestra scritta a mano, molto peggio.
+ /// Selettore di cartella. Usa quello di WinForms perché WPF non ne offre uno.
///
private static string? PickFolder(string description, string startFrom)
{
@@ -199,38 +87,134 @@ namespace AutoBidder
}
///
- /// Applica i percorsi dopo un salvataggio. Se sono cambiati serve un riavvio: gli
- /// archivi hanno già in memoria i dati letti dalla posizione precedente, e
- /// spostarli a caldo rischierebbe di mescolarli.
+ /// Dopo un salvataggio con la cartella dei database cambiata: chiede se spostare i
+ /// file nella nuova posizione, li sposta a database chiusi, e riavvia. Senza
+ /// riavvio gli archivi terrebbero in memoria i dati letti dalla posizione vecchia.
///
- private void ApplyDataFolderSettings(AppSettings settings, string previousData, string previousStats)
+ private void ApplyDatabaseFolderSetting(AppSettings settings, string previousFolder)
{
- AppPaths.Configure(settings.DataFolder, settings.StatsFolder, settings.LogFolder, settings.DatabasePath);
- AppPaths.EnsureFolders();
+ var newFolder = settings.DatabaseFolder.Length > 0 ? settings.DatabaseFolder : AppPaths.DefaultDatabaseFolder;
- // I campi tornano a mostrare i percorsi effettivi: chi ne ha svuotato uno deve
- // vedere subito qual è il predefinito che ha appena scelto.
- RefreshDataFolderFields();
+ try { newFolder = Path.GetFullPath(newFolder); } catch { }
- if (previousData == AppPaths.DataFolder && previousStats == AppPaths.StatsFolder) return;
+ if (string.Equals(previousFolder.TrimEnd('\\'), newFolder.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase))
+ {
+ RefreshDataFolderFields();
+ return;
+ }
- MessageBox.Show(this,
- "Le cartelle dei dati sono cambiate.\n\n" +
- $"Dati: {AppPaths.DataFolder}\nStatistiche: {AppPaths.StatsFolder}\n\n" +
- "Riavvia l'applicazione perché vengano usate davvero. I file già scritti " +
- "restano dove sono: puoi spostarli a mano nella nuova posizione.",
- "Cartelle dei dati", MessageBoxButton.OK, MessageBoxImage.Information);
+ var answer = MessageBox.Show(this,
+ $"La cartella dei database cambia da\n{previousFolder}\na\n{newFolder}\n\n" +
+ "Vuoi spostare i database esistenti nella nuova cartella?\n\n" +
+ "Sì: sposto i file e riavvio l'applicazione.\n" +
+ "No: la nuova cartella parte vuota (i vecchi file restano dove sono) e riavvio.\n" +
+ "Annulla: torno alla cartella di prima.",
+ "Cartella dei database", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
- Log($"[DATI] Nuovi percorsi impostati (riavvio necessario)", LogLevel.Warning);
+ if (answer == MessageBoxResult.Cancel)
+ {
+ settings.DatabaseFolder = string.Equals(previousFolder, AppPaths.DefaultDatabaseFolder, StringComparison.OrdinalIgnoreCase) ? "" : previousFolder;
+ SettingsManager.Save(settings);
+ RefreshDataFolderFields();
+ return;
+ }
+
+ try
+ {
+ if (answer == MessageBoxResult.Yes)
+ {
+ // Prima si chiudono: un file SQLite aperto non si sposta, e il giornale
+ // WAL va con lui.
+ AuctionDatabase.CloseInstance();
+ OperationalDatabase.CloseInstance();
+ Directory.CreateDirectory(newFolder);
+
+ var moved = 0;
+ foreach (var db in new[] { AppPaths.DatabaseFile, AppPaths.OperationalDatabaseFile })
+ {
+ foreach (var f in AppPaths.DatabaseFiles(db))
+ {
+ var target = Path.Combine(newFolder, Path.GetFileName(f));
+ if (File.Exists(target)) File.Delete(target);
+ File.Move(f, target);
+ moved++;
+ }
+ }
+
+ Log($"[DATI] Database spostati in {newFolder} ({moved} file). Riavvio.", LogLevel.Warning);
+ }
+ else
+ {
+ Log($"[DATI] Nuova cartella dei database: {newFolder} (vuota). Riavvio.", LogLevel.Warning);
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this, $"Spostamento non riuscito: {ex.Message}\n\nLa cartella resta quella di prima.",
+ "Cartella dei database", MessageBoxButton.OK, MessageBoxImage.Error);
+ settings.DatabaseFolder = string.Equals(previousFolder, AppPaths.DefaultDatabaseFolder, StringComparison.OrdinalIgnoreCase) ? "" : previousFolder;
+ SettingsManager.Save(settings);
+ RefreshDataFolderFields();
+ return;
+ }
+
+ RestartApplication();
}
- // ── Esportazione dello storico ───────────────────────────────────
+ /// Riavvia l'applicazione: lo stesso eseguibile, e questa istanza si chiude.
+ private void RestartApplication()
+ {
+ try
+ {
+ var exe = Environment.ProcessPath;
+ if (!string.IsNullOrEmpty(exe))
+ Process.Start(new ProcessStartInfo { FileName = exe, UseShellExecute = true });
+ }
+ catch (Exception ex)
+ {
+ Log($"[APP] Riavvio non riuscito: {ex.Message}. Riapri l'applicazione a mano.", LogLevel.Error);
+ }
+
+ Application.Current.Shutdown();
+ }
+
+ // ── Esportazione dello storico: chiede sempre dove ───────────────
private void ExportStatsCsvButton_Click(object sender, RoutedEventArgs e)
- => RunExport(StatsExporter.ExportCsv(), "CSV");
+ {
+ var path = AskWhereToSave("Esporta lo storico in CSV", $"storico-aste-{DateTime.Now:yyyyMMdd-HHmm}.csv", "CSV (foglio di calcolo)|*.csv");
+ if (path == null) return;
+ RunExport(StatsExporter.ExportCsv(path), "CSV");
+ }
private void ExportStatsJsonButton_Click(object sender, RoutedEventArgs e)
- => RunExport(StatsExporter.ExportJson(), "JSON");
+ {
+ var path = AskWhereToSave("Esporta lo storico in JSON", $"storico-aste-{DateTime.Now:yyyyMMdd-HHmm}.json", "JSON|*.json");
+ if (path == null) return;
+ RunExport(StatsExporter.ExportJson(path), "JSON");
+ }
+
+ /// La finestra «Salva con nome»: nessun percorso predefinito, decide l'utente.
+ private string? AskWhereToSave(string title, string fileName, string filter)
+ {
+ try
+ {
+ var dialog = new Microsoft.Win32.SaveFileDialog
+ {
+ Title = title,
+ FileName = fileName,
+ Filter = filter,
+ OverwritePrompt = true,
+ AddExtension = true
+ };
+ return dialog.ShowDialog(this) == true ? dialog.FileName : null;
+ }
+ catch (Exception ex)
+ {
+ Log($"[ESPORTA] Finestra di salvataggio non disponibile: {ex.Message}", LogLevel.Error);
+ return null;
+ }
+ }
private void RunExport(StatsExporter.Result result, string format)
{
@@ -250,7 +234,7 @@ namespace AutoBidder
if (answer == MessageBoxResult.Yes)
{
- OpenFolder(Path.GetDirectoryName(result.Path) ?? AppPaths.ExportFolder);
+ OpenFolder(Path.GetDirectoryName(result.Path) ?? AppPaths.ConfigRoot);
}
}
}
diff --git a/Mimante/Core/MainWindow.Export.cs b/Mimante/Core/MainWindow.Export.cs
deleted file mode 100644
index 0a3e5d6..0000000
--- a/Mimante/Core/MainWindow.Export.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Windows;
-using AutoBidder.Utilities;
-
-namespace AutoBidder
-{
- ///
- /// Scheda Esporta: raccoglie in un unico file le aste che rispondono ai filtri.
- ///
- /// Qui c'è solo il collegamento fra il pannello e
- /// . La scrittura può durare qualche secondo — con gli
- /// eventi inclusi si leggono decine di dossier — quindi gira su un thread di lavoro:
- /// bloccare l'interfaccia mentre il motore sta seguendo un'asta non è accettabile.
- ///
- public partial class MainWindow
- {
- private async void Export_ExportRequested(object sender, RoutedEventArgs e)
- {
- try
- {
- var filter = Export.BuildFilter();
-
- Log("[ESPORTA] Esportazione in corso…", LogLevel.Info);
-
- var result = await System.Threading.Tasks.Task
- .Run(() => AuctionExporter.Export(filter))
- .ConfigureAwait(true);
-
- Export.ShowResult(result);
-
- Log(result.Success
- ? $"[ESPORTA] {result.Message} → {result.Path}"
- : $"[ESPORTA] {result.Message}",
- result.Success ? LogLevel.Success : LogLevel.Warning);
- }
- catch (Exception ex)
- {
- Log($"[ESPORTA] Esportazione non riuscita: {ex.Message}", LogLevel.Error);
- }
- }
-
- private void Export_OpenFolderClicked(object sender, RoutedEventArgs e)
- {
- try
- {
- Directory.CreateDirectory(AppPaths.ExportFolder);
- OpenInExplorer(AppPaths.ExportFolder);
- }
- catch (Exception ex)
- {
- Log($"[ESPORTA] Apertura cartella non riuscita: {ex.Message}", LogLevel.Error);
- }
- }
-
- private void Export_ClearExportsClicked(object sender, RoutedEventArgs e)
- {
- var answer = MessageBox.Show(this,
- "Cancello tutti i file nella cartella delle esportazioni? Sono copie: si rigenerano in qualunque momento.",
- "Svuota le esportazioni", MessageBoxButton.YesNo, MessageBoxImage.Question);
-
- if (answer != MessageBoxResult.Yes) return;
-
- try
- {
- var (files, bytes) = StatsWipe.ClearExports();
- Log($"[ESPORTA] Cartella svuotata: {files} file, {bytes / 1024.0:F0} KB liberati", LogLevel.Success);
- }
- catch (Exception ex)
- {
- Log($"[ESPORTA] Svuotamento non riuscito: {ex.Message}", LogLevel.Error);
- }
- }
-
- private void Export_OpenLastFileClicked(object sender, RoutedEventArgs e)
- {
- var path = Export.LastExportPath;
- if (string.IsNullOrEmpty(path) || !File.Exists(path)) return;
-
- try
- {
- Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
- }
- catch (Exception ex)
- {
- Log($"[ESPORTA] Apertura file non riuscita: {ex.Message}", LogLevel.Error);
- }
- }
-
- ///
- /// Apre una cartella in Esplora risorse. Sta qui perché la usano più schede: i
- /// pulsanti "apri" dei percorsi, le esportazioni e i registri.
- ///
- internal void OpenInExplorer(string folder)
- {
- try
- {
- if (string.IsNullOrWhiteSpace(folder)) return;
-
- Directory.CreateDirectory(folder);
-
- Process.Start(new ProcessStartInfo
- {
- FileName = folder,
- UseShellExecute = true
- });
- }
- catch (Exception ex)
- {
- Log($"[CARTELLE] Impossibile aprire {folder}: {ex.Message}", LogLevel.Error);
- }
- }
- }
-}
diff --git a/Mimante/Core/MainWindow.Products.cs b/Mimante/Core/MainWindow.Products.cs
index 9bcf6a9..835b18f 100644
--- a/Mimante/Core/MainWindow.Products.cs
+++ b/Mimante/Core/MainWindow.Products.cs
@@ -217,6 +217,73 @@ namespace AutoBidder
Log("[PRODOTTI] Elenco svuotato", LogLevel.Info);
}
+ private void Products_ClearAllClicked(object sender, RoutedEventArgs e)
+ {
+ var answer = MessageBox.Show(this,
+ "Pulizia completa dei prodotti:\n\n" +
+ "• tolgo tutti i prodotti dall'elenco, con i loro limiti\n" +
+ "• azzero le statistiche per prodotto (aste concluse, prezzi tipici, consigli)\n\n" +
+ "Lo storico delle aste non viene toccato. Le aste già nel monitor restano dove sono.\n\nProcedo?",
+ "Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ WatchedProductsStore.Clear();
+ ProductStatsStore.ClearStatistics();
+ LoadProducts();
+ MarkWatchedProducts(_catalogAuctions);
+
+ Log("[PRODOTTI] Pulizia completa: elenco e statistiche per prodotto azzerati", LogLevel.Warning);
+ }
+
+ private void Products_UnwatchAllClicked(object sender, RoutedEventArgs e)
+ {
+ var accese = _productViewModels.Count(p => p.IsWatched);
+ if (accese == 0)
+ {
+ MessageBox.Show(this, "Nessun prodotto ha la stellina accesa.",
+ "Prodotti", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ var answer = MessageBox.Show(this,
+ $"Spengo la stellina di {accese} prodotti?\n\n" +
+ "Nessun prodotto verrà più cercato in automatico. Restano tutti in elenco con i loro limiti.",
+ "Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Question);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ var n = WatchedProductsStore.UnwatchAll();
+ LoadProducts();
+ MarkWatchedProducts(_catalogAuctions);
+
+ Log($"[PRODOTTI] Stelline spente su {n} prodotti", LogLevel.Info);
+ }
+
+ private void Products_RemoveUnwatchedClicked(object sender, RoutedEventArgs e)
+ {
+ var spente = _productViewModels.Count(p => !p.IsWatched);
+ if (spente == 0)
+ {
+ MessageBox.Show(this, "Tutti i prodotti in elenco hanno la stellina accesa: non c'è nulla da togliere.",
+ "Prodotti", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ var answer = MessageBox.Show(this,
+ $"Tolgo dall'elenco i {spente} prodotti senza stellina?\n\n" +
+ "Si perdono i limiti impostati su quei prodotti. Le statistiche per prodotto restano.",
+ "Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Question);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ var n = WatchedProductsStore.RemoveUnwatched();
+ LoadProducts();
+ MarkWatchedProducts(_catalogAuctions);
+
+ Log($"[PRODOTTI] Tolti {n} prodotti senza stellina", LogLevel.Info);
+ }
+
///
/// Scrive nei limiti del prodotto i valori consigliati dallo storico — minimo e
/// massimo insieme, perché è la coppia a definire la finestra in cui ha senso
diff --git a/Mimante/Core/MainWindow.Risk.cs b/Mimante/Core/MainWindow.Risk.cs
index 3ee58bd..51709d3 100644
--- a/Mimante/Core/MainWindow.Risk.cs
+++ b/Mimante/Core/MainWindow.Risk.cs
@@ -1,63 +1,117 @@
using System;
using System.IO;
using System.Windows;
-using AutoBidder.Services;
+using AutoBidder.Data;
using AutoBidder.Utilities;
namespace AutoBidder
{
///
- /// Kill-switch dalla barra del monitor e percorso del database dalle impostazioni.
+ /// L'interruttore dell'apprendimento nella barra del monitor, e le due operazioni
+ /// estreme delle Impostazioni: cancellare tutti i dati tranne la login, e tornare alle
+ /// impostazioni di fabbrica.
///
public partial class MainWindow
{
- private void AuctionMonitor_KillSwitchChanged(object sender, RoutedEventArgs e)
+ // ── Apprendimento acceso / spento ────────────────────────────────
+
+ private bool _syncingLearningToggle;
+
+ private void AuctionMonitor_LearningToggled(object sender, RoutedEventArgs e)
{
- var on = AuctionMonitor.KillSwitchToggle.IsChecked == true;
- RiskManager.SetKillSwitch(on);
+ if (_syncingLearningToggle) return;
+
+ var on = AuctionMonitor.LearningToggle.IsChecked == true;
+ var settings = SettingsManager.Load();
+ if (settings.LearningEnabled == on) return;
+
+ settings.LearningEnabled = on;
+ SettingsManager.Save(settings);
Log(on
- ? "[RISCHIO] Kill-switch ATTIVO: nessuna puntata dal vivo finché resta acceso (file KILL_SWITCH)"
- : "[RISCHIO] Kill-switch tolto: le puntate dal vivo possono ripartire",
- on ? LogLevel.Warning : LogLevel.Success);
+ ? "[APPRENDIMENTO] Acceso: il motore decide con valore atteso, regime, duello e anticipo adattivo"
+ : "[APPRENDIMENTO] SPENTO: si punta solo entro i limiti (prezzo, puntate, budget, fascia oraria) con l'anticipo fisso",
+ on ? LogLevel.Success : LogLevel.Warning);
- try { Settings.RefreshRiskStatus(); } catch { }
+ try { Learning.Refresh(); } catch { }
}
- /// Il kill-switch cambiato dalle Impostazioni: l'interruttore in barra deve seguirlo.
- private void Settings_KillSwitchChanged(object sender, RoutedEventArgs e) => RefreshKillSwitchToggle();
-
- /// Allinea l'interruttore in barra allo stato vero del file sentinella.
- private void RefreshKillSwitchToggle()
- {
- try { AuctionMonitor.KillSwitchToggle.IsChecked = RiskManager.KillSwitchActive; } catch { }
- }
-
- private void Settings_BrowseDatabaseFileClicked(object sender, RoutedEventArgs e)
+ /// Allinea l'interruttore in barra a ciò che dicono le impostazioni.
+ private void RefreshLearningToggle()
{
try
{
- var current = AppPaths.DatabaseFile;
- var dialog = new Microsoft.Win32.SaveFileDialog
- {
- Title = "Scegli dove tenere il database",
- FileName = Path.GetFileName(current),
- InitialDirectory = Path.GetDirectoryName(current),
- Filter = "Database SQLite (*.sqlite)|*.sqlite|Tutti i file|*.*",
- OverwritePrompt = false,
- CheckPathExists = true
- };
+ _syncingLearningToggle = true;
+ AuctionMonitor.LearningToggle.IsChecked = SettingsManager.Load().LearningEnabled;
+ }
+ catch { }
+ finally { _syncingLearningToggle = false; }
+ }
- if (dialog.ShowDialog(this) == true)
- Settings.DatabaseFileTextBox.Text = dialog.FileName;
+ // ── Manutenzione ─────────────────────────────────────────────────
+
+ private void Settings_WipeAllDataClicked(object sender, RoutedEventArgs e)
+ {
+ var answer = MessageBox.Show(this,
+ "Cancello TUTTI i dati dell'applicazione:\n\n" +
+ "• i due database (aste, puntate, decisioni, prodotti seguiti, aste nel monitor, modelli appresi, registri)\n" +
+ "• le copie di sicurezza\n" +
+ "• la vecchia cartella Dati delle versioni precedenti\n\n" +
+ "Restano la login (sessione, riferimenti del sito, browser) e le impostazioni.\n" +
+ "L'operazione non è reversibile e l'applicazione si riavvia subito.\n\nProcedo?",
+ "Elimina tutti i dati", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ var conferma = MessageBox.Show(this,
+ "Ultima conferma: cancellare tutto tranne la login?",
+ "Elimina tutti i dati", MessageBoxButton.YesNo, MessageBoxImage.Stop);
+
+ if (conferma != MessageBoxResult.Yes) return;
+
+ try { _auctionMonitor.Stop(); } catch { }
+
+ var errors = AppDataWipe.WipeAllExceptLogin();
+ if (errors.Count > 0)
+ {
+ MessageBox.Show(this, "Alcuni file non si sono potuti cancellare:\n\n" + string.Join("\n", errors) +
+ "\n\nL'applicazione si riavvia comunque.", "Elimina tutti i dati", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+
+ RestartApplication();
+ }
+
+ private void Settings_ResetSettingsClicked(object sender, RoutedEventArgs e)
+ {
+ var answer = MessageBox.Show(this,
+ "Riporto tutte le impostazioni ai valori di fabbrica e riavvio l'applicazione.\n\n" +
+ "I dati (database, prodotti, storico, apprendimento) e la login non vengono toccati.\n\nProcedo?",
+ "Impostazioni di fabbrica", MessageBoxButton.YesNo, MessageBoxImage.Question);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ try { _auctionMonitor.Stop(); } catch { }
+ AppDataWipe.ResetSettings();
+ RestartApplication();
+ }
+
+ private void Settings_ExportAppLogClicked(object sender, RoutedEventArgs e)
+ {
+ var path = AskWhereToSave("Esporta il registro applicativo", $"registro-{DateTime.Now:yyyyMMdd-HHmm}.txt", "Testo|*.txt");
+ if (path == null) return;
+
+ try
+ {
+ var n = OperationalDatabase.Instance.ExportLog(OperationalDatabase.AppLog, path);
+ var m = OperationalDatabase.Instance.ExportLog(OperationalDatabase.FreeBidsLog,
+ Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path) + "-puntate.txt"));
+ Log($"[REGISTRI] Esportate {n} righe del registro applicativo e {m} del riscatto puntate in {Path.GetDirectoryName(path)}", LogLevel.Success);
+ OpenFolder(Path.GetDirectoryName(path)!);
}
catch (Exception ex)
{
- Log($"[DATI] Scelta del database non riuscita: {ex.Message}", LogLevel.Error);
+ Log($"[REGISTRI] Esportazione non riuscita: {ex.Message}", LogLevel.Error);
}
}
-
- private void Settings_OpenDatabaseFolderClicked(object sender, RoutedEventArgs e)
- => OpenFolder(Path.GetDirectoryName(AppPaths.DatabaseFile) ?? AppPaths.Root);
}
}
diff --git a/Mimante/Core/MainWindow.Statistics.cs b/Mimante/Core/MainWindow.Statistics.cs
index c1220b4..e097371 100644
--- a/Mimante/Core/MainWindow.Statistics.cs
+++ b/Mimante/Core/MainWindow.Statistics.cs
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
+using AutoBidder.Models;
using AutoBidder.Utilities;
namespace AutoBidder
@@ -48,12 +49,12 @@ namespace AutoBidder
LoadStatistics();
}
- /// Apre la cartella contenente il file storico JSON.
+ /// Apre la cartella dei database.
private void OpenStatsFolderButton_Click(object sender, RoutedEventArgs e)
{
try
{
- var folder = AppPaths.StatsFolder;
+ var folder = AppPaths.DatabaseFolder;
if (!string.IsNullOrEmpty(folder) && System.IO.Directory.Exists(folder))
{
Process.Start(new ProcessStartInfo
@@ -69,6 +70,105 @@ namespace AutoBidder
}
}
+ // ── Selezione multipla ───────────────────────────────────────────
+
+ private void SelectAllStatsButton_Click(object sender, RoutedEventArgs e)
+ {
+ StatsDetailGrid.Focus();
+ StatsDetailGrid.SelectAll();
+ }
+
+ private void DeleteSelectedStatsButton_Click(object sender, RoutedEventArgs e)
+ => DeleteSelectedHistoryRows();
+
+ private void StatsDetailGrid_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key != System.Windows.Input.Key.Delete) return;
+ e.Handled = true;
+ DeleteSelectedHistoryRows();
+ }
+
+ ///
+ /// Toglie dallo storico le righe selezionate. Prima la copia di sicurezza, poi la
+ /// cancellazione, poi le statistiche per prodotto ricalcolate da ciò che resta.
+ ///
+ private void DeleteSelectedHistoryRows()
+ {
+ var selected = StatsDetailGrid.SelectedItems.OfType().ToList();
+ if (selected.Count == 0)
+ {
+ MessageBox.Show(this, "Seleziona prima una o più righe dello storico (Ctrl+clic, Maiusc+clic, o «Seleziona tutte»).",
+ "Storico", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ var answer = MessageBox.Show(this,
+ (selected.Count == 1
+ ? $"Elimino dallo storico \"{selected[0].Name}\"?"
+ : $"Elimino dallo storico {selected.Count} aste?") +
+ "\n\nCon loro vanno via puntate, interrogazioni, reset e decisioni registrate. " +
+ "Prima salvo una copia nei backup e poi ricalcolo le statistiche per prodotto.",
+ "Storico", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ try
+ {
+ var backup = CompletedAuctionsStore.BackupNow();
+ var n = CompletedAuctionsStore.Remove(selected.Select(r => r.AuctionId).ToList());
+ var ricostruite = ProductStatsStore.RebuildFromHistory();
+
+ LoadStatistics();
+ LoadProducts();
+
+ Log($"[STORICO] Eliminate {n} aste; statistiche per prodotto ricalcolate su {ricostruite} aste rimaste. Copia di sicurezza in {backup}",
+ LogLevel.Success);
+ }
+ catch (Exception ex)
+ {
+ Log($"[STORICO] Eliminazione non riuscita: {ex.Message}", LogLevel.Error);
+ }
+ }
+
+ ///
+ /// Svuota tutto lo storico in un colpo: osservazioni, decisioni, misure, contabilità
+ /// del rischio e statistiche per prodotto. Prodotti seguiti e apprendimento restano.
+ ///
+ private void WipeHistoryButton_Click(object sender, RoutedEventArgs e)
+ {
+ var count = CompletedAuctionsStore.LoadAll().Count;
+ if (count == 0)
+ {
+ MessageBox.Show(this, "Lo storico è già vuoto.", "Storico", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ var answer = MessageBox.Show(this,
+ $"Svuoto completamente lo storico ({count} aste)?\n\n" +
+ "Vanno via tutte le aste concluse con puntate, interrogazioni, reset, decisioni, misure di rete " +
+ "e contabilità del rischio, e le statistiche per prodotto tornano a zero.\n" +
+ "Prima salvo una copia nei backup. Prodotti seguiti, aste nel monitor e apprendimento restano.\n\nProcedo?",
+ "Storico", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ try
+ {
+ var backup = CompletedAuctionsStore.BackupNow();
+ CompletedAuctionsStore.Clear();
+ ProductStatsStore.ClearStatistics();
+
+ LoadStatistics();
+ LoadProducts();
+
+ Log($"[STORICO] Svuotato: {count} aste tolte. Copia di sicurezza in {backup}", LogLevel.Warning);
+ }
+ catch (Exception ex)
+ {
+ Log($"[STORICO] Svuotamento non riuscito: {ex.Message}", LogLevel.Error);
+ }
+ }
+
///
/// Azzera le statistiche registrate: il dialogo dice cosa c'è e quanto pesa, e
/// l'utente sceglie voce per voce. Vedi .
diff --git a/Mimante/Data/AuctionDatabase.cs b/Mimante/Data/AuctionDatabase.cs
index 62b638d..1c10a0f 100644
--- a/Mimante/Data/AuctionDatabase.cs
+++ b/Mimante/Data/AuctionDatabase.cs
@@ -1,64 +1,46 @@
using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
using System.Threading;
using AutoBidder.Utilities;
namespace AutoBidder.Data
{
///
- /// L'archivio dell'applicazione: un solo file SQLite con tutto ciò che si osserva e
+ /// L'archivio delle osservazioni: un solo file SQLite con tutto ciò che si osserva e
/// si decide — aste, puntate, interrogazioni, reset, nostre puntate, decisioni del
- /// motore, misure di rete, puntatori, sessioni.
+ /// motore, misure di rete, puntatori, sessioni, contabilità del rischio.
///
/// Perché un database. Prima c'erano un file JSON per asta (i dossier),
- /// un file JSON con lo storico compatto, uno con le statistiche per prodotto, uno con
- /// le misure dell'anticipo: quattro copie parziali degli stessi fatti, da tenere
- /// allineate, e nessuna interrogabile. Qui ogni fatto sta in una riga sola, con un
- /// indice, e una domanda come «quante puntate manuali fra le 10 e le 13 sui buoni da
- /// 10 € sono rimaste senza risposta?» è una riga di SQL invece di una lettura di otto
- /// gigabyte.
+ /// un file JSON con lo storico compatto, uno con le statistiche per prodotto: copie
+ /// parziali degli stessi fatti, da tenere allineate, e nessuna interrogabile. Qui ogni
+ /// fatto sta in una riga sola, con un indice, e una domanda come «quante puntate
+ /// manuali fra le 10 e le 13 sui buoni da 10 € sono rimaste senza risposta?» è una
+ /// riga di SQL invece di una lettura di otto gigabyte.
///
- /// Come scrive. Mai sul thread che segue le aste. Ogni scrittura finisce
- /// in una coda e un thread dedicato la svuota a blocchi, in una transazione ogni
- /// qualche centinaio di righe: SQLite fa migliaia di inserimenti al secondo così, e
- /// nessuno sul percorso della puntata aspetta il disco. Chi deve leggere ciò che ha
- /// appena scritto chiama .
- ///
- /// Come legge. Su una seconda connessione, sotto un lucchetto. Con il
- /// giornale in modalità WAL le letture non bloccano la scrittura e viceversa.
- ///
- /// Dove sta. In %LocalAppData%\AutoBidder\Database: non in
- /// Documenti, che può essere sincronizzata da OneDrive — un file SQLite sotto
- /// sincronizzazione è il modo classico di corromperlo.
+ /// Il motore (coda di scrittura, letture in WAL) è in .
+ /// I dati d'esercizio e i registri stanno nell'altro file, .
///
- public sealed class AuctionDatabase : IDisposable
+ public sealed class AuctionDatabase : SqliteDatabase
{
public const int SchemaVersion = 1;
private static readonly object InstanceSync = new();
private static AuctionDatabase? _instance;
- /// Righe di registro (errori di scrittura, migrazioni): chi si aggancia le scrive dove vuole.
- public static event Action? OnLog;
-
- /// L'archivio dell'applicazione, aperto al primo uso nel percorso di .
+ /// L'archivio dell'applicazione, aperto al primo uso in .
public static AuctionDatabase Instance
{
get
{
lock (InstanceSync)
{
- if (_instance == null || _instance._disposed)
+ if (_instance == null || _instance.IsDisposed)
_instance = Open(AppPaths.DatabaseFile);
return _instance;
}
}
}
- /// Chiude l'archivio dell'applicazione svuotando la coda. Da chiamare all'uscita.
+ /// Chiude l'archivio dell'applicazione svuotando la coda. All'uscita, o prima di spostarlo.
public static void CloseInstance()
{
lock (InstanceSync)
@@ -68,118 +50,48 @@ namespace AutoBidder.Data
}
}
- private readonly SqliteConnection _writer;
- private readonly SqliteConnection _reader;
- private readonly object _readSync = new();
-
- // ── Coda di scrittura ────────────────────────────────────────────
-
- private abstract class Pending
- {
- public long Seq;
- }
-
- private sealed class Statement : Pending
- {
- public string Sql = "";
- public object?[] Args = Array.Empty
public static AuctionRecorder? OpenFor(AuctionInfo auction, AppSettings settings)
{
- if (auction == null || !settings.WriteAuctionDossiers) return null;
+ if (auction == null || !settings.RecordAuctions) return null;
if (string.IsNullOrEmpty(auction.AuctionId)) return null;
return Open.GetOrAdd(auction.AuctionId, _ => Create(auction, settings));
@@ -190,7 +190,7 @@ namespace AutoBidder.Data
leadMaxMs = settings.LeadMaxMs,
valueCheckEnabled = settings.ValueCheckEnabled,
minSavingsPercentage = settings.MinSavingsPercentage,
- rawPollsIncluded = settings.DossierIncludeRawPolls
+ rawPollsIncluded = settings.RecordPolls
}, Json);
var me = string.IsNullOrEmpty(CurrentUsername) ? null : CurrentUsername;
diff --git a/Mimante/Data/OperationalDatabase.cs b/Mimante/Data/OperationalDatabase.cs
new file mode 100644
index 0000000..dd0e9eb
--- /dev/null
+++ b/Mimante/Data/OperationalDatabase.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using AutoBidder.Utilities;
+
+namespace AutoBidder.Data
+{
+ ///
+ /// Il database d'esercizio: tutto ciò che l'applicazione scrive per sé e non per
+ /// l'analisi. Sta in un file a parte (esercizio.sqlite) accanto a quello delle
+ /// osservazioni, così l'archivio delle aste resta un archivio e questo resta piccolo.
+ ///
+ /// Documenti. Le aste nel monitor, i prodotti seguiti, le statistiche per
+ /// prodotto, le promozioni riscattate, il modello appreso, il profilo, la latenza:
+ /// ognuno era un file JSON riscritto per intero a ogni modifica. Qui sono righe di una
+ /// tabella documents, chiave → JSON: la scrittura è atomica (una riga in una
+ /// transazione, niente file a metà), e c'è un solo file da salvare.
+ ///
+ /// Registri. Il registro applicativo e quello del riscatto puntate sono
+ /// tabelle: una riga per evento, con data e livello. Si interrogano, si esportano in
+ /// testo dalle Impostazioni, e si cancellano per età senza girare cartelle.
+ ///
+ public sealed class OperationalDatabase : SqliteDatabase
+ {
+ public const int SchemaVersion = 1;
+
+ private static readonly object InstanceSync = new();
+ private static OperationalDatabase? _instance;
+
+ public static OperationalDatabase Instance
+ {
+ get
+ {
+ lock (InstanceSync)
+ {
+ if (_instance == null || _instance.IsDisposed)
+ _instance = Open(AppPaths.OperationalDatabaseFile);
+ return _instance;
+ }
+ }
+ }
+
+ public static void CloseInstance()
+ {
+ lock (InstanceSync)
+ {
+ _instance?.Dispose();
+ _instance = null;
+ }
+ }
+
+ /// Vero quando l'istanza è aperta e pronta: chi scrive nei registri lo controlla prima.
+ public static bool IsOpen
+ {
+ get { lock (InstanceSync) return _instance != null && !_instance.IsDisposed; }
+ }
+
+ private OperationalDatabase(string path) : base(path, SchemaV1, SchemaVersion, "AutoBidder.Esercizio") { }
+
+ public static OperationalDatabase Open(string path) => new(path);
+
+ private const string SchemaV1 = """
+ -- Un documento JSON per chiave: aste nel monitor, prodotti seguiti,
+ -- statistiche per prodotto, promozioni riscattate, modelli appresi.
+ CREATE TABLE IF NOT EXISTS documents(
+ key TEXT PRIMARY KEY,
+ json TEXT NOT NULL,
+ updated_at TEXT NOT NULL);
+
+ -- Il registro applicativo: una riga per evento.
+ CREATE TABLE IF NOT EXISTS app_log(
+ id INTEGER PRIMARY KEY,
+ ts TEXT NOT NULL,
+ level TEXT NOT NULL,
+ message TEXT NOT NULL);
+ CREATE INDEX IF NOT EXISTS ix_app_log_ts ON app_log(ts);
+
+ -- Il registro del riscatto puntate.
+ CREATE TABLE IF NOT EXISTS freebids_log(
+ id INTEGER PRIMARY KEY,
+ ts TEXT NOT NULL,
+ level TEXT NOT NULL,
+ message TEXT NOT NULL);
+ CREATE INDEX IF NOT EXISTS ix_freebids_log_ts ON freebids_log(ts);
+ """;
+
+ // ── Documenti ────────────────────────────────────────────────────
+
+ /// Il JSON del documento, o null se non c'è.
+ public string? GetDocument(string key)
+ {
+ Flush();
+ return ScalarString("SELECT json FROM documents WHERE key = ?1", key);
+ }
+
+ ///
+ /// Il documento, oppure — se manca — il contenuto del vecchio file JSON indicato,
+ /// che viene importato una volta e da allora vive qui. Il file resta dov'è.
+ ///
+ public string? GetDocumentOrImport(string key, string? legacyFile)
+ {
+ var json = GetDocument(key);
+ if (json != null) return json;
+
+ try
+ {
+ if (string.IsNullOrEmpty(legacyFile) || !File.Exists(legacyFile)) return null;
+ json = File.ReadAllText(legacyFile);
+ if (string.IsNullOrWhiteSpace(json)) return null;
+
+ SetDocument(key, json);
+ Log($"[DATABASE] Importato {System.IO.Path.GetFileName(legacyFile)} come documento «{key}»");
+ return json;
+ }
+ catch (Exception ex)
+ {
+ Log($"[DATABASE] Importazione di {legacyFile} non riuscita: {ex.Message}");
+ return null;
+ }
+ }
+
+ public void SetDocument(string key, string json)
+ {
+ Enqueue("INSERT INTO documents(key, json, updated_at) VALUES(?1, ?2, ?3) " +
+ "ON CONFLICT(key) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at",
+ key, json, Now());
+ }
+
+ public void DeleteDocument(string key) => Enqueue("DELETE FROM documents WHERE key = ?1", key);
+
+ /// Cancella tutti i documenti con quel prefisso (es. «ml/»).
+ public void DeleteDocuments(string prefix) => Enqueue("DELETE FROM documents WHERE key LIKE ?1", prefix + "%");
+
+ public IReadOnlyList<(string Key, long Bytes, DateTime UpdatedAt)> Documents()
+ {
+ Flush();
+ return Query("SELECT key, LENGTH(json) AS bytes, updated_at FROM documents ORDER BY key")
+ .Select(r => (r.Str("key"), r.Long("bytes"), r.Date("updated_at") ?? DateTime.MinValue))
+ .ToList();
+ }
+
+ // ── Registri ─────────────────────────────────────────────────────
+
+ public const string AppLog = "app_log";
+ public const string FreeBidsLog = "freebids_log";
+
+ public void WriteLog(string table, string level, string message)
+ {
+ if (table != AppLog && table != FreeBidsLog) return;
+ Enqueue($"INSERT INTO {table}(ts, level, message) VALUES(?1, ?2, ?3)", Now(), level, message ?? "");
+ }
+
+ /// Cancella le righe più vecchie di tanti giorni. Restituisce quante.
+ public int PurgeLogs(int days)
+ {
+ if (days <= 0) return 0;
+ var cutoff = Iso(DateTime.Now.AddDays(-days));
+ var removed = 0;
+ foreach (var table in new[] { AppLog, FreeBidsLog })
+ {
+ removed += (int)ScalarLong($"SELECT COUNT(*) AS n FROM {table} WHERE ts < ?1", cutoff);
+ Enqueue($"DELETE FROM {table} WHERE ts < ?1", cutoff);
+ }
+ Flush();
+ return removed;
+ }
+
+ /// Le ultime righe di un registro, dalla più recente.
+ public List<(DateTime Ts, string Level, string Message)> ReadLog(string table, int max = 2000, DateTime? since = null)
+ {
+ Flush();
+ var rows = since is { } s
+ ? Query($"SELECT ts, level, message FROM {table} WHERE ts >= ?1 ORDER BY id DESC LIMIT ?2", Iso(s), max)
+ : Query($"SELECT ts, level, message FROM {table} ORDER BY id DESC LIMIT ?1", max);
+
+ return rows.Select(r => (r.Date("ts") ?? DateTime.MinValue, r.Str("level"), r.Str("message"))).ToList();
+ }
+
+ /// Scrive un registro in un file di testo leggibile (una riga per evento, ora locale).
+ public int ExportLog(string table, string destination, int days = 0)
+ {
+ Flush();
+ var rows = days > 0
+ ? Query($"SELECT ts, level, message FROM {table} WHERE ts >= ?1 ORDER BY id", Iso(DateTime.Now.AddDays(-days)))
+ : Query($"SELECT ts, level, message FROM {table} ORDER BY id");
+
+ var sb = new StringBuilder();
+ foreach (var r in rows)
+ {
+ var ts = r.Date("ts")?.ToLocalTime() ?? DateTime.MinValue;
+ sb.Append(ts.ToString("yyyy-MM-dd HH:mm:ss.fff")).Append(" [").Append(r.Str("level")).Append("] ").AppendLine(r.Str("message"));
+ }
+
+ var folder = System.IO.Path.GetDirectoryName(destination);
+ if (!string.IsNullOrEmpty(folder)) Directory.CreateDirectory(folder);
+ File.WriteAllText(destination, sb.ToString(), new UTF8Encoding(true));
+ return rows.Count;
+ }
+ }
+}
diff --git a/Mimante/Data/SqliteDatabase.cs b/Mimante/Data/SqliteDatabase.cs
new file mode 100644
index 0000000..9cc98d1
--- /dev/null
+++ b/Mimante/Data/SqliteDatabase.cs
@@ -0,0 +1,408 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+
+namespace AutoBidder.Data
+{
+ ///
+ /// Il motore comune dei due database dell'applicazione: un file SQLite, una coda di
+ /// scrittura svuotata a blocchi da un thread dedicato, una connessione di lettura a
+ /// parte con giornale WAL.
+ ///
+ /// Come scrive. Mai sul thread che chiama. Ogni scrittura finisce in coda
+ /// e il thread del database la esegue in una transazione ogni qualche centinaio di
+ /// righe: SQLite fa migliaia di inserimenti al secondo così, e nessuno sul percorso
+ /// della puntata aspetta il disco. Chi deve leggere ciò che ha appena scritto chiama
+ /// .
+ ///
+ /// Come legge. Su una seconda connessione, sotto un lucchetto. Con il
+ /// giornale WAL le letture non bloccano la scrittura e viceversa.
+ ///
+ public abstract class SqliteDatabase : IDisposable
+ {
+ /// Righe di registro (errori di scrittura, migrazioni): chi si aggancia le scrive dove vuole.
+ public static event Action? OnLog;
+
+ protected static void Log(string message) => OnLog?.Invoke(message);
+
+ private readonly SqliteConnection _writer;
+ private readonly SqliteConnection _reader;
+ private readonly object _readSync = new();
+
+ private abstract class Pending { public long Seq; }
+
+ private sealed class Statement : Pending
+ {
+ public string Sql = "";
+ public object?[] Args = Array.Empty();
+ }
+
+ private sealed class Batch : Pending
+ {
+ public List<(string Sql, object?[] Args)> Ops = new();
+ }
+
+ private sealed class Custom : Pending
+ {
+ public Action Action = _ => { };
+ public Exception? Error;
+ public ManualResetEventSlim Done = new(false);
+ }
+
+ private readonly Queue _queue = new();
+ private readonly object _queueSync = new();
+ private long _lastEnqueued;
+ private long _lastCommitted;
+ private readonly Thread _writerThread;
+ private volatile bool _disposed;
+ private volatile bool _stop;
+
+ private long _writesCommitted;
+ private long _writesFailed;
+
+ public long WritesCommitted => Interlocked.Read(ref _writesCommitted);
+ public long WritesFailed => Interlocked.Read(ref _writesFailed);
+ public int QueueLength { get { lock (_queueSync) return _queue.Count; } }
+ public bool IsDisposed => _disposed;
+
+ public string Path { get; }
+
+ public static string LibraryVersion => SqliteConnection.LibraryVersion;
+
+ protected SqliteDatabase(string path, string schemaScript, int schemaVersion, string threadName)
+ {
+ Path = path;
+
+ var folder = System.IO.Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(folder)) Directory.CreateDirectory(folder);
+
+ _writer = new SqliteConnection(path);
+ Configure(_writer);
+ Migrate(_writer, schemaScript, schemaVersion);
+ OnOpened(_writer);
+
+ _reader = new SqliteConnection(path);
+ Configure(_reader);
+
+ _writerThread = new Thread(WriterLoop)
+ {
+ Name = threadName,
+ IsBackground = true,
+ Priority = ThreadPriority.BelowNormal
+ };
+ _writerThread.Start();
+ }
+
+ /// Chiamato con la connessione di scrittura, dopo lo schema, prima del thread.
+ protected virtual void OnOpened(SqliteConnection writer) { }
+
+ /// Chiamato dal thread di scrittura per ogni istruzione eseguita.
+ protected virtual void OnStatementExecuted(string sql) { }
+
+ private static void Configure(SqliteConnection c)
+ {
+ c.Execute("PRAGMA journal_mode=WAL");
+ c.Execute("PRAGMA synchronous=NORMAL");
+ c.Execute("PRAGMA foreign_keys=ON");
+ c.Execute("PRAGMA temp_store=MEMORY");
+ }
+
+ private void Migrate(SqliteConnection c, string schema, int target)
+ {
+ var version = (int)c.Query("PRAGMA user_version")[0].Long("user_version");
+ if (version >= target) return;
+
+ c.ExecuteScript(schema);
+ c.Execute($"PRAGMA user_version={target}");
+ Log($"[DATABASE] Schema versione {target} in {c.Path} — SQLite {LibraryVersion}");
+ }
+
+ // ── Scrittura ────────────────────────────────────────────────────
+
+ public void Enqueue(string sql, params object?[] args)
+ {
+ if (_disposed) return;
+ Push(new Statement { Sql = sql, Args = args });
+ }
+
+ public void EnqueueBatch(IEnumerable<(string Sql, object?[] Args)> ops)
+ {
+ if (_disposed) return;
+ var batch = new Batch();
+ batch.Ops.AddRange(ops);
+ if (batch.Ops.Count == 0) return;
+ Push(batch);
+ }
+
+ public void RunOnWriter(Action action, int timeoutMs = 60000)
+ {
+ if (_disposed) throw new ObjectDisposedException(GetType().Name);
+ var custom = new Custom { Action = action };
+ Push(custom);
+ if (!custom.Done.Wait(timeoutMs)) throw new TimeoutException("il database non ha risposto in tempo");
+ if (custom.Error != null) throw custom.Error;
+ }
+
+ public void ExecuteNow(string sql, params object?[] args)
+ {
+ Enqueue(sql, args);
+ Flush();
+ }
+
+ private void Push(Pending p)
+ {
+ lock (_queueSync)
+ {
+ p.Seq = ++_lastEnqueued;
+ _queue.Enqueue(p);
+ Monitor.PulseAll(_queueSync);
+ }
+ }
+
+ public bool Flush(int timeoutMs = 15000)
+ {
+ long target;
+ lock (_queueSync) target = _lastEnqueued;
+
+ var deadline = Environment.TickCount64 + timeoutMs;
+ lock (_queueSync)
+ {
+ while (Interlocked.Read(ref _lastCommitted) < target)
+ {
+ var left = deadline - Environment.TickCount64;
+ if (left <= 0 || _disposed) return Interlocked.Read(ref _lastCommitted) >= target;
+ Monitor.Wait(_queueSync, (int)Math.Min(left, 500));
+ }
+ }
+ return true;
+ }
+
+ private void WriterLoop()
+ {
+ var buffer = new List(512);
+
+ while (!_stop)
+ {
+ buffer.Clear();
+
+ lock (_queueSync)
+ {
+ while (_queue.Count == 0 && !_stop) Monitor.Wait(_queueSync, 250);
+ while (_queue.Count > 0 && buffer.Count < 2000) buffer.Add(_queue.Dequeue());
+ }
+
+ if (buffer.Count == 0) continue;
+
+ var i = 0;
+ while (i < buffer.Count)
+ {
+ switch (buffer[i])
+ {
+ case Statement:
+ var j = i;
+ while (j < buffer.Count && buffer[j] is Statement) j++;
+ RunStatements(buffer, i, j);
+ i = j;
+ break;
+
+ case Batch b:
+ RunBatch(b);
+ i++;
+ break;
+
+ case Custom c:
+ try { c.Action(_writer); }
+ catch (Exception ex) { c.Error = ex; }
+ finally { MarkCommitted(c.Seq); c.Done.Set(); }
+ i++;
+ break;
+
+ default:
+ i++;
+ break;
+ }
+ }
+ }
+ }
+
+ private void RunStatements(List buffer, int from, int to)
+ {
+ long lastSeq = 0;
+ var inTx = false;
+
+ try
+ {
+ _writer.Execute("BEGIN IMMEDIATE");
+ inTx = true;
+
+ for (var k = from; k < to; k++)
+ {
+ var s = (Statement)buffer[k];
+ lastSeq = s.Seq;
+ try
+ {
+ var stmt = _writer.Cached(s.Sql);
+ stmt.Bind(s.Args);
+ stmt.RunToEnd();
+ Interlocked.Increment(ref _writesCommitted);
+ OnStatementExecuted(s.Sql);
+ }
+ catch (SqliteException ex)
+ {
+ Interlocked.Increment(ref _writesFailed);
+ Log($"[DATABASE] Scrittura rifiutata: {ex.Message}");
+ }
+ }
+
+ _writer.Execute("COMMIT");
+ inTx = false;
+ }
+ catch (Exception ex)
+ {
+ Log($"[DATABASE] Transazione non riuscita: {ex.Message}");
+ if (inTx) { try { _writer.Execute("ROLLBACK"); } catch { } }
+ Interlocked.Add(ref _writesFailed, to - from);
+ }
+ finally
+ {
+ MarkCommitted(Math.Max(lastSeq, buffer[to - 1].Seq));
+ }
+ }
+
+ private void RunBatch(Batch b)
+ {
+ var inTx = false;
+ try
+ {
+ _writer.Execute("BEGIN IMMEDIATE");
+ inTx = true;
+
+ foreach (var (sql, args) in b.Ops)
+ {
+ var stmt = _writer.Cached(sql);
+ stmt.Bind(args);
+ stmt.RunToEnd();
+ OnStatementExecuted(sql);
+ }
+
+ _writer.Execute("COMMIT");
+ inTx = false;
+ Interlocked.Add(ref _writesCommitted, b.Ops.Count);
+ }
+ catch (Exception ex)
+ {
+ Log($"[DATABASE] Blocco annullato: {ex.Message}");
+ if (inTx) { try { _writer.Execute("ROLLBACK"); } catch { } }
+ Interlocked.Add(ref _writesFailed, b.Ops.Count);
+ }
+ finally
+ {
+ MarkCommitted(b.Seq);
+ }
+ }
+
+ private void MarkCommitted(long seq)
+ {
+ lock (_queueSync)
+ {
+ if (seq > _lastCommitted) Interlocked.Exchange(ref _lastCommitted, seq);
+ Monitor.PulseAll(_queueSync);
+ }
+ }
+
+ // ── Lettura ──────────────────────────────────────────────────────
+
+ public List Query(string sql, params object?[] args)
+ {
+ if (_disposed) return new List();
+ lock (_readSync) return _reader.Query(sql, args);
+ }
+
+ public SqliteRow? QueryOne(string sql, params object?[] args)
+ {
+ var rows = Query(sql, args);
+ return rows.Count > 0 ? rows[0] : null;
+ }
+
+ public long ScalarLong(string sql, params object?[] args)
+ {
+ var v = QueryOne(sql, args)?.At(0);
+ return v switch { long l => l, double d => (long)d, string s when long.TryParse(s, out var x) => x, _ => 0 };
+ }
+
+ public double ScalarDouble(string sql, params object?[] args)
+ {
+ var v = QueryOne(sql, args)?.At(0);
+ return v switch { long l => l, double d => d, string s when double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var x) => x, _ => 0 };
+ }
+
+ public string? ScalarString(string sql, params object?[] args) => QueryOne(sql, args)?.At(0)?.ToString();
+
+ public long Count(string table) => ScalarLong($"SELECT COUNT(*) AS n FROM {table}");
+
+ // ── Manutenzione ─────────────────────────────────────────────────
+
+ /// Copia coerente dell'intero archivio in un altro file (VACUUM INTO).
+ public void BackupTo(string destination)
+ {
+ Flush();
+ if (File.Exists(destination)) File.Delete(destination);
+ var folder = System.IO.Path.GetDirectoryName(destination);
+ if (!string.IsNullOrEmpty(folder)) Directory.CreateDirectory(folder);
+
+ RunOnWriter(c => c.Execute("VACUUM INTO ?1", destination));
+ }
+
+ /// Byte occupati sul disco (file principale più giornale WAL).
+ public long SizeBytes
+ {
+ get
+ {
+ long total = 0;
+ foreach (var f in new[] { Path, Path + "-wal" })
+ {
+ try { if (File.Exists(f)) total += new FileInfo(f).Length; } catch { }
+ }
+ return total;
+ }
+ }
+
+ /// Compatta il file. Fuori da ogni transazione, sul thread del database.
+ public void Vacuum()
+ {
+ try { RunOnWriter(c => c.Execute("VACUUM")); } catch { /* lo spazio si recupera la prossima volta */ }
+ }
+
+ // ── Attrezzi ─────────────────────────────────────────────────────
+
+ /// Le date si scrivono in ISO 8601 UTC: ordinano bene e non dipendono dal fuso.
+ public static string Iso(DateTime dt)
+ {
+ var utc = dt.Kind switch
+ {
+ DateTimeKind.Utc => dt,
+ DateTimeKind.Local => dt.ToUniversalTime(),
+ _ => DateTime.SpecifyKind(dt, DateTimeKind.Local).ToUniversalTime()
+ };
+ return utc.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
+ }
+
+ public static string Now() => Iso(DateTime.UtcNow);
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+
+ Flush(5000);
+ _stop = true;
+ lock (_queueSync) Monitor.PulseAll(_queueSync);
+ try { _writerThread.Join(3000); } catch { }
+
+ _disposed = true;
+ lock (_readSync) _reader.Dispose();
+ _writer.Dispose();
+ }
+ }
+}
diff --git a/Mimante/Dialogs/WipeStatsDialog.xaml b/Mimante/Dialogs/WipeStatsDialog.xaml
index 3427710..ecd3f63 100644
--- a/Mimante/Dialogs/WipeStatsDialog.xaml
+++ b/Mimante/Dialogs/WipeStatsDialog.xaml
@@ -26,24 +26,14 @@
ToolTip="I totali per prodotto (aste viste, prezzi medi, puntate del vincitore). Le opzioni scelte nella scheda Prodotti restano: si azzerano i numeri, non le scelte."
Checked="Option_Changed" Unchecked="Option_Changed"/>
-
-
-
-
-
@@ -55,7 +45,7 @@
+ Text="Per cancellare tutto — database, copie di sicurezza, cartella vecchia — c'è «Elimina tutti i dati» nelle Impostazioni."/>
diff --git a/Mimante/Dialogs/WipeStatsDialog.xaml.cs b/Mimante/Dialogs/WipeStatsDialog.xaml.cs
index 1f2bd51..b5cb075 100644
--- a/Mimante/Dialogs/WipeStatsDialog.xaml.cs
+++ b/Mimante/Dialogs/WipeStatsDialog.xaml.cs
@@ -29,8 +29,6 @@ namespace AutoBidder.Dialogs
{
History = ChkHistory.IsChecked == true,
ProductStats = ChkProducts.IsChecked == true,
- BidLeadMeasures = ChkBidLead.IsChecked == true,
- Exports = ChkExports.IsChecked == true,
LegacyArchives = ChkLegacy.IsChecked == true,
Learning = ChkLearning.IsChecked == true
};
@@ -43,17 +41,13 @@ namespace AutoBidder.Dialogs
ChkHistory.Content = $"Osservazioni nel database: aste, puntate, interrogazioni, decisioni ({_sizes.HistoryAuctions} aste, {Fmt(_sizes.HistoryBytes)})";
ChkProducts.Content = $"Statistiche per prodotto ({Fmt(_sizes.ProductsBytes)})";
- ChkBidLead.Content = $"Misure dell'anticipo effettivo ({_sizes.BidLeadSamples} misure, {Fmt(_sizes.BidLeadBytes)})";
- ChkExports.Content = $"Esportazioni ({_sizes.ExportFiles} file, {Fmt(_sizes.ExportBytes)})";
- ChkLegacy.Content = $"Archivi JSON delle versioni precedenti ({_sizes.LegacyFiles} file, {Fmt(_sizes.LegacyBytes)})";
+ ChkLegacy.Content = $"Vecchia cartella Dati delle versioni precedenti ({_sizes.LegacyFiles} file, {Fmt(_sizes.LegacyBytes)})";
ChkLearning.Content = $"Apprendimento: modello, profilo, latenza ({Fmt(_sizes.LearningBytes)})";
var o = ReadOptions();
long totale = 0;
if (o.History) totale += _sizes.HistoryBytes;
if (o.ProductStats) totale += _sizes.ProductsBytes;
- if (o.BidLeadMeasures) totale += _sizes.BidLeadBytes;
- if (o.Exports) totale += _sizes.ExportBytes;
if (o.LegacyArchives) totale += _sizes.LegacyBytes;
if (o.Learning) totale += _sizes.LearningBytes;
@@ -61,7 +55,7 @@ namespace AutoBidder.Dialogs
? "Nessuna voce scelta."
: $"Si liberano circa {Fmt(totale)}." +
(o.History ? " Lo storico viene prima copiato nei backup." : "") +
- (o.Learning ? " L'apprendimento ripartirà da zero e ristudierà i dossier al prossimo avvio." : "");
+ (o.Learning ? " L'apprendimento ripartirà da zero e ristudierà le aste al prossimo avvio." : "");
WipeButton.IsEnabled = !o.Nothing;
}
diff --git a/Mimante/Engine/AuctionRunner.cs b/Mimante/Engine/AuctionRunner.cs
index eef37ba..f74bb1b 100644
--- a/Mimante/Engine/AuctionRunner.cs
+++ b/Mimante/Engine/AuctionRunner.cs
@@ -242,7 +242,8 @@ namespace AutoBidder.Engine
if (_auction.BidLeadIsManual && _auction.BidBeforeDeadlineMs > 0)
return _auction.BidBeforeDeadlineMs;
- var lead = settings.AdaptiveLeadEnabled
+ // Con l'apprendimento spento vale l'anticipo fisso: è l'interruttore in barra.
+ var lead = settings.AdaptiveLeadEnabled && settings.LearningEnabled
? Ml.LatencyModel.RecommendedLeadMs(settings)
: settings.DefaultBidBeforeDeadlineMs;
diff --git a/Mimante/Modifiche.txt b/Mimante/Machine learning per aste al centesimo su Bidoo.txt
similarity index 100%
rename from Mimante/Modifiche.txt
rename to Mimante/Machine learning per aste al centesimo su Bidoo.txt
diff --git a/Mimante/MainWindow.xaml b/Mimante/MainWindow.xaml
index 14f2a0a..96f4dd1 100644
--- a/Mimante/MainWindow.xaml
+++ b/Mimante/MainWindow.xaml
@@ -69,12 +69,6 @@
Style="{StaticResource VerticalTabButton}"
Checked="TabDatiStatistici_Checked"/>
-
-
-
-
-
@@ -271,41 +258,63 @@
+
-
-
+
+
+
+
+
-
+
+
+
+
+
+
@@ -316,7 +325,9 @@
Background="{DynamicResource Brush.Surface}"
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1" CornerRadius="12">
+ SelectionMode="Extended" SelectionUnit="FullRow"
+ Background="Transparent" Margin="4"
+ PreviewKeyDown="StatsDetailGrid_PreviewKeyDown">
@@ -355,17 +366,11 @@
ConnectSessionClicked="Settings_ConnectSessionClicked"
DisconnectSessionClicked="Settings_DisconnectSessionClicked"
OpenBrowserLoginClicked="Settings_OpenBrowserLoginClicked"
- ComputeBidLeadAdviceClicked="Settings_ComputeBidLeadAdviceClicked"
- ClearBidLeadStatsClicked="Settings_ClearBidLeadStatsClicked"
- BrowseDataFolderClicked="Settings_BrowseDataFolderClicked"
- BrowseStatsFolderClicked="Settings_BrowseStatsFolderClicked"
- OpenDataFolderClicked="Settings_OpenDataFolderClicked"
- OpenStatsFolderClicked="Settings_OpenStatsFolderClicked"
- BrowseLogFolderClicked="Settings_BrowseLogFolderClicked"
- OpenLogFolderClicked="Settings_OpenLogFolderClicked"
- BrowseDatabaseFileClicked="Settings_BrowseDatabaseFileClicked"
- OpenDatabaseFolderClicked="Settings_OpenDatabaseFolderClicked"
- KillSwitchChanged="Settings_KillSwitchChanged"/>
+ BrowseDatabaseFolderClicked="Settings_BrowseDatabaseFolderClicked"
+ WipeAllDataClicked="Settings_WipeAllDataClicked"
+ ResetSettingsClicked="Settings_ResetSettingsClicked"
+ ExportAppLogClicked="Settings_ExportAppLogClicked"
+ OpenDatabaseFolderClicked="Settings_OpenDatabaseFolderClicked"/>
\ No newline at end of file
diff --git a/Mimante/MainWindow.xaml.cs b/Mimante/MainWindow.xaml.cs
index 1b44550..f652cdc 100644
--- a/Mimante/MainWindow.xaml.cs
+++ b/Mimante/MainWindow.xaml.cs
@@ -102,7 +102,7 @@ namespace AutoBidder
// Inizializza servizi
_auctionMonitor = new AuctionMonitor();
Learning.AuctionsProvider = () => _auctionMonitor.GetAuctions();
- RefreshKillSwitchToggle();
+ RefreshLearningToggle();
// ? NUOVO: Inizializza SessionService
InitializeSessionService();
@@ -220,25 +220,35 @@ namespace AutoBidder
///
/// Costruisce le schede prodotto la prima volta, ricavandole dallo storico già
- /// raccolto.
+ /// raccolto, e le ricostruisce una volta sola quando cambia il modo di contare.
///
/// Senza, chi ha già mesi di aste alle spalle aprirebbe un file vuoto e
/// dovrebbe aspettare le aste future per vedere qualcosa — proprio i numeri che
- /// servono adesso per decidere i limiti. Gira una sola volta e su un thread di
- /// lavoro: rileggere l'archivio all'avvio non deve ritardare la finestra.
+ /// servono adesso per decidere i limiti. Gira su un thread di lavoro: rileggere
+ /// l'archivio all'avvio non deve ritardare la finestra.
+ ///
+ /// Il contrassegno nel database d'esercizio dice con quale «generazione» le
+ /// schede sono state contate. La 2 è quella in cui la chiusura di un'asta arriva
+ /// una volta sola: prima ogni asta conclusa veniva contata due volte, e le schede
+ /// costruite allora vanno rifatte dallo storico, che è sempre stato giusto.
///
private void EnsureProductCardsBuilt()
{
+ const string Marker = "products-generation";
+ const string Generation = "2";
+
try
{
- if (System.IO.File.Exists(AppPaths.ProductsFile)) return;
+ var op = Data.OperationalDatabase.Instance;
+ if (ProductStatsStore.GetAll().Count > 0 && op.GetDocument(Marker) == Generation) return;
_ = System.Threading.Tasks.Task.Run(() =>
{
var count = ProductStatsStore.RebuildFromHistory();
+ op.SetDocument(Marker, Generation);
Dispatcher.BeginInvoke(() =>
- Log($"[PRODOTTI] Schede prodotto costruite dallo storico ({count} aste)", LogLevel.Info));
+ Log($"[PRODOTTI] Schede prodotto ricostruite dallo storico ({count} aste)", LogLevel.Info));
});
}
catch (Exception ex)
diff --git a/Mimante/Ml/LEGGIMI.md b/Mimante/Ml/LEGGIMI.md
index 8c644ec..83d396d 100644
--- a/Mimante/Ml/LEGGIMI.md
+++ b/Mimante/Ml/LEGGIMI.md
@@ -40,10 +40,18 @@ obbligatoria; le aste in *Attiva* puntano davvero, dentro i paletti del gestore
## 1. Cosa si registra: il database
-Un solo file SQLite (`%LocalAppData%\AutoBidder\Database\autobidder.sqlite`, cambiabile
-dalle Impostazioni; mai in una cartella sincronizzata). Le scritture vanno in coda e un
-thread dedicato le svuota a blocchi in transazioni: nulla sul percorso della puntata
-aspetta il disco. Le letture usano una seconda connessione con giornale WAL.
+Due file SQLite in `%LocalAppData%\AutoBidder\Database` (cartella cambiabile dalle
+Impostazioni, con spostamento dei file e riavvio; mai una cartella sincronizzata):
+
+- `autobidder.sqlite` — le **osservazioni**: le tabelle qui sotto.
+- `esercizio.sqlite` — l'**esercizio**: i documenti JSON dell'applicazione (aste nel
+ monitor, prodotti seguiti, statistiche per prodotto, promozioni riscattate, modelli
+ appresi con prefisso `ml/`) e i registri applicativo e del riscatto puntate. I file
+ JSON delle versioni precedenti vengono importati la prima volta e lasciati dove sono.
+
+Le scritture vanno in coda e un thread dedicato per database le svuota a blocchi in
+transazioni: nulla sul percorso della puntata aspetta il disco. Le letture usano una
+seconda connessione con giornale WAL. Il motore comune è `SqliteDatabase`.
| tabella | cosa contiene |
|---|---|
@@ -85,9 +93,11 @@ il «Compralo Ora»), `Δ = 0,01 €`, `fee ≈ 1 €`, `c` il costo della punta
**Copertura «Compralo Ora».** Chi perde può comprare al listino `L` recuperando le puntate
spese come credito: la perdita finale, se si esercita, è `L − V` qualunque sia il numero di
-puntate. Quindi `c_perdita = c` finché `spese + c ≤ L − V`, zero dopo. Per un buono al
-nominale (`V = L`) la copertura è totale dal primo centesimo. È un'ipotesi forte, e per
-questo è un'impostazione (`Copertura «Compralo Ora»`), accesa per scelta dell'utente.
+puntate. Quindi `c_perdita = c` finché `spese + c ≤ L − V`, zero dopo. Con `V = L` la
+copertura è totale dal primo centesimo e si punterebbe sempre, a qualunque probabilità: in
+shadow è successo (BID con P dell'1%). Per questo la copertura è un'impostazione
+(`Copertura «Compralo Ora»`) **e vale solo per i prodotti con «Valore reale €» scritto**:
+con V lasciato al listino la puntata si conta persa, come senza copertura.
**Null-model.** Profitto atteso per puntata = 0; numero critico di puntatori `V/c`
(Hinnosaar). **Sopravvivenza empirica:** dalla tabella `resets`, P(vittoria) a un dato
@@ -109,13 +119,19 @@ dell'istante di fuoco:
QUANDO: anticipo adattivo
```
-1. **Rischio** — `RiskManager.Authorize`: kill-switch (file `KILL_SWITCH` in
- `%LocalAppData%\AutoBidder`, anche dalla barra del monitor), HALT persistente, stop-loss
- del giorno, drawdown dal picco, tetto di spesa del giorno, aste in gioco insieme. Lo
- stop-loss e il drawdown mettono in HALT, che sopravvive al riavvio e si toglie a mano.
- La contabilità è in `risk_ledger`: ogni puntata riuscita un'uscita, ogni vincita
+1. **Rischio** — `RiskManager.Authorize`: HALT persistente, stop-loss del giorno,
+ drawdown dal picco, tetto di spesa del giorno, aste in gioco insieme. Lo stop-loss e
+ il drawdown mettono in HALT, che sopravvive al riavvio e si toglie a mano. La
+ contabilità è in `risk_ledger`: ogni puntata riuscita un'uscita, ogni vincita
un'entrata al valore reale meno prezzo, fee e spedizione.
2. **Paletti** — fascia oraria sospesa (globale, per prodotto, per asta), tetti di puntate.
+
+ **L'interruttore dell'apprendimento** (il robot nella barra del monitor, accanto ad
+ Avvia/Osserva/Ferma; `LearningEnabled`) taglia qui la pipeline: spento, i passi 3–5 non
+ girano e l'anticipo è quello fisso delle impostazioni. Si punta solo entro i limiti
+ dell'utente — prezzo minimo e massimo, puntate, budget, fascia oraria, rischio — come
+ un cecchino senza cervello. Le aste vengono comunque registrate e studiate: quando lo
+ si riaccende il modello non ha perso niente.
3. **Duello** — cinque risposte di fila con la firma dell'autopuntata (6 ± 2,5 s) = macchina
armata dall'altra parte; si smette e si informa il regime.
4. **Valore atteso** — il modello dà P(senza risposta); `Theory` la trasforma in euro.
@@ -217,8 +233,11 @@ si spegne il vivo. La posizione di default è che un vantaggio consistente sia i
Scheda **Apprendimento**: stato e pesi del modello, ultime decisioni, profilo per prodotto,
valutazione, autonomia sul momento (latenza e regime per asta), shadow (policy a confronto,
-campione/sfidante, bandit), simulatore. Scheda **Impostazioni › Gestione del rischio**:
-paletti, kill-switch, HALT e contabilità. Scheda **Prodotti**: colonna «Valore reale €».
+campione/sfidante, bandit), simulatore. Barra del **monitor**: l'interruttore
+dell'apprendimento. Scheda **Impostazioni › Gestione del rischio**: paletti, HALT e
+contabilità; **Impostazioni › Database** e **Manutenzione**: cartella dei database, «Elimina
+tutti i dati (tranne la login)», «Impostazioni di fabbrica». Scheda **Prodotti**: colonna
+«Valore reale €».
Nel registro di ogni asta: `[SHADOW] Avrei puntato…`, `[REGIME] Calmo → Sfogo…`, `[TIMING]
… anticipo adattivo…`, `⛔ Strategia blocca: …`.
@@ -238,8 +257,8 @@ Nel registro di ogni asta: `[SHADOW] Avrei puntato…`, `[REGIME] Calmo → Sfog
| | stato |
|---|---|
-| M1 rischio, kill-switch, HALT, shadow per asta | fatto |
-| M2 database SQLite, registrazione, sessioni, misure di rete | fatto (si parte da zero) |
+| M1 rischio, HALT, interruttore dell'apprendimento, shadow per asta | fatto |
+| M2 database SQLite (osservazioni + esercizio), registrazione, sessioni, misure di rete | fatto (si parte da zero) |
| M3 teoria, simulatore con avversari tarati, rigiocata | fatto |
| M4 modelli: logistico, rete neurale, profilo, hazard/Kaplan-Meier, Brier | fatto |
| M5 policy: valore atteso + regime, bandit Thompson, agente Q conservativo | fatto (Q solo nel simulatore) |
diff --git a/Mimante/Ml/LatencyModel.cs b/Mimante/Ml/LatencyModel.cs
index 535d360..8453e1d 100644
--- a/Mimante/Ml/LatencyModel.cs
+++ b/Mimante/Ml/LatencyModel.cs
@@ -51,7 +51,7 @@ namespace AutoBidder.Ml
private static int _bidsTotal;
private static bool _loaded;
- private static string FilePath => Path.Combine(AppPaths.StatsFolder, "Apprendimento", "latenza.json");
+ private const string DocKey = "ml/latenza";
// ── Alimentazione ────────────────────────────────────────────────
@@ -163,8 +163,9 @@ namespace AutoBidder.Ml
_loaded = true;
try
{
- if (!File.Exists(FilePath)) return;
- var s = JsonSerializer.Deserialize(File.ReadAllText(FilePath));
+ var json = Data.OperationalDatabase.Instance.GetDocumentOrImport(DocKey, Path.Combine(AppPaths.LegacyLearningFolder, "latenza.json"));
+ if (json == null) return;
+ var s = JsonSerializer.Deserialize(json);
if (s == null) return;
_marginMs = Math.Clamp(s.MarginMs, 150, 2000);
_lateBids = s.LateBids;
@@ -190,8 +191,7 @@ namespace AutoBidder.Ml
RoundTrips = _roundTrips.ToArray()
});
}
- Directory.CreateDirectory(Path.GetDirectoryName(FilePath)!);
- File.WriteAllText(FilePath, json);
+ Data.OperationalDatabase.Instance.SetDocument(DocKey, json);
}
catch { /* la prossima puntata riproverà */ }
}
@@ -200,7 +200,7 @@ namespace AutoBidder.Ml
public static void Forget()
{
ResetForTests();
- try { if (File.Exists(FilePath)) File.Delete(FilePath); } catch { }
+ try { Data.OperationalDatabase.Instance.DeleteDocument(DocKey); } catch { }
}
/// Solo per i test.
diff --git a/Mimante/Ml/LearningService.cs b/Mimante/Ml/LearningService.cs
index 6ff369d..f775e83 100644
--- a/Mimante/Ml/LearningService.cs
+++ b/Mimante/Ml/LearningService.cs
@@ -90,14 +90,21 @@ namespace AutoBidder.Ml
// ── Percorsi ─────────────────────────────────────────────────────
- private static string Folder => Path.Combine(AppPaths.StatsFolder, "Apprendimento");
- private static string ModelFile => Path.Combine(Folder, "modello-puntate.json");
- private static string ProfileFile => Path.Combine(Folder, "profilo-aste.json");
- private static string ChallengerFile => Path.Combine(Folder, "sfidante-rete.json");
- private static string BanditFile => Path.Combine(Folder, "bandit.json");
- private static string BrierFile => Path.Combine(Folder, "brier.txt");
- private static string CounterFile => Path.Combine(Folder, "aste-apprese.txt");
- private static string LegacyManifestFile => Path.Combine(Folder, "dossier-appresi.txt");
+ // Tutto ciò che il modello sa vive nel database d'esercizio, come documenti con
+ // prefisso «ml/». I file della cartella Apprendimento delle versioni precedenti
+ // vengono importati la prima volta.
+ private const string DocModel = "ml/modello-puntate";
+ private const string DocProfile = "ml/profilo-aste";
+ private const string DocChallenger = "ml/sfidante-rete";
+ private const string DocBandit = "ml/bandit";
+ private const string DocBrier = "ml/brier";
+ private const string DocCounter = "ml/aste-apprese";
+ private const string DocEvaluation = "ml/valutazione";
+
+ private static OperationalDatabase Docs => OperationalDatabase.Instance;
+
+ private static string? Doc(string key, string legacyFileName) =>
+ Docs.GetDocumentOrImport(key, Path.Combine(AppPaths.LegacyLearningFolder, legacyFileName));
// ── Stato, per l'interfaccia e il registro ───────────────────────
@@ -178,12 +185,10 @@ namespace AutoBidder.Ml
// ── Valutazione dall'applicazione ────────────────────────────────
- private static string EvaluationFile => Path.Combine(Folder, "valutazione.txt");
-
/// L'ultimo rapporto di valutazione salvato, se c'è.
public static string? LastEvaluation()
{
- try { return File.Exists(EvaluationFile) ? File.ReadAllText(EvaluationFile) : null; }
+ try { return Doc(DocEvaluation, "valutazione.txt"); }
catch { return null; }
}
@@ -200,8 +205,7 @@ namespace AutoBidder.Ml
try
{
- Directory.CreateDirectory(Folder);
- lock (SaveSync) File.WriteAllText(EvaluationFile, $"Valutazione del {DateTime.Now:dd/MM/yyyy HH:mm}\n\n" + report.Text);
+ Docs.SetDocument(DocEvaluation, $"Valutazione del {DateTime.Now:dd/MM/yyyy HH:mm}\n\n" + report.Text);
}
catch { /* il rapporto e' comunque restituito */ }
@@ -235,6 +239,7 @@ namespace AutoBidder.Ml
///
public static void Forget()
{
+ lock (Sync) _closedSeen.Clear();
lock (Sync)
{
_model = new OnlineLogit(BidFeatures.Size);
@@ -249,11 +254,7 @@ namespace AutoBidder.Ml
try
{
- lock (SaveSync)
- {
- foreach (var f in new[] { ModelFile, ProfileFile, CounterFile, LegacyManifestFile, EvaluationFile, ChallengerFile, BanditFile, BrierFile })
- if (File.Exists(f)) File.Delete(f);
- }
+ lock (SaveSync) Docs.DeleteDocuments("ml/");
}
catch (Exception ex)
{
@@ -282,17 +283,17 @@ namespace AutoBidder.Ml
try
{
- if (File.Exists(ModelFile))
- _model = OnlineLogit.FromJson(File.ReadAllText(ModelFile), BidFeatures.Size);
- if (File.Exists(ProfileFile))
- _profile = AuctionProfileStats.FromJson(File.ReadAllText(ProfileFile));
- if (File.Exists(ChallengerFile))
- _challenger = NeuralNet.FromJson(File.ReadAllText(ChallengerFile), new[] { BidFeatures.Size, 32, 16, 1 });
- if (File.Exists(BanditFile))
- _bandit = ThompsonBandit.FromJson(File.ReadAllText(BanditFile));
- if (File.Exists(BrierFile))
+ if (Doc(DocModel, "modello-puntate.json") is { } model)
+ _model = OnlineLogit.FromJson(model, BidFeatures.Size);
+ if (Doc(DocProfile, "profilo-aste.json") is { } profile)
+ _profile = AuctionProfileStats.FromJson(profile);
+ if (Doc(DocChallenger, "sfidante-rete.json") is { } challenger)
+ _challenger = NeuralNet.FromJson(challenger, new[] { BidFeatures.Size, 32, 16, 1 });
+ if (Doc(DocBandit, "bandit.json") is { } bandit)
+ _bandit = ThompsonBandit.FromJson(bandit);
+ if (Doc(DocBrier, "brier.txt") is { } brierText)
{
- var parts = File.ReadAllText(BrierFile).Split(';');
+ var parts = brierText.Split(';');
if (parts.Length >= 3 &&
double.TryParse(parts[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var bl) &&
double.TryParse(parts[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var bn) &&
@@ -322,15 +323,26 @@ namespace AutoBidder.Ml
///
private static int ReadCounter()
{
- foreach (var f in new[] { CounterFile, LegacyManifestFile })
+ // Il contatore nuovo, oppure quello delle versioni precedenti, che stava in
+ // testa all'elenco dei dossier letti: non si azzera un modello che ha già
+ // studiato migliaia di aste.
+ var text = Doc(DocCounter, "aste-apprese.txt");
+ if (text == null)
{
- if (!File.Exists(f)) continue;
- foreach (var line in File.ReadLines(f))
+ try
{
- var l = line.Trim();
- if (l.StartsWith("#appresi=", StringComparison.Ordinal) && int.TryParse(l["#appresi=".Length..], out var n)) return n;
- if (int.TryParse(l, out var plain)) return plain;
+ var legacy = Path.Combine(AppPaths.LegacyLearningFolder, "dossier-appresi.txt");
+ if (File.Exists(legacy)) text = File.ReadAllText(legacy);
}
+ catch { }
+ }
+ if (string.IsNullOrEmpty(text)) return 0;
+
+ foreach (var line in text.Split('\n'))
+ {
+ var l = line.Trim();
+ if (l.StartsWith("#appresi=", StringComparison.Ordinal) && int.TryParse(l["#appresi=".Length..], out var n)) return n;
+ if (int.TryParse(l, out var plain)) return plain;
}
return 0;
}
@@ -356,13 +368,12 @@ namespace AutoBidder.Ml
lock (SaveSync)
{
- Directory.CreateDirectory(Folder);
- File.WriteAllText(ModelFile, model);
- File.WriteAllText(ProfileFile, profile);
- File.WriteAllText(ChallengerFile, challenger);
- File.WriteAllText(BanditFile, bandit);
- File.WriteAllText(BrierFile, brier);
- File.WriteAllText(CounterFile, "#appresi=" + learned + Environment.NewLine);
+ Docs.SetDocument(DocModel, model);
+ Docs.SetDocument(DocProfile, profile);
+ Docs.SetDocument(DocChallenger, challenger);
+ Docs.SetDocument(DocBandit, bandit);
+ Docs.SetDocument(DocBrier, brier);
+ Docs.SetDocument(DocCounter, "#appresi=" + learned);
}
}
catch (Exception ex)
@@ -527,6 +538,19 @@ namespace AutoBidder.Ml
// ── Chiusura di un'asta: si impara subito ────────────────────────
+ private static readonly HashSet _closedSeen = new(StringComparer.Ordinal);
+
+ /// Vero se l'asta ha già learned_at nel database: studiata in una sessione precedente.
+ private static bool AlreadyLearned(string auctionId)
+ {
+ try
+ {
+ Db.Flush();
+ return Db.ScalarString("SELECT learned_at FROM auctions WHERE auction_id = ?1", auctionId) != null;
+ }
+ catch { return false; }
+ }
+
///
/// Da chiamare quando un'asta è chiusa e la sua registrazione è stata completata.
/// Aggiorna il profilo con lo storico appena registrato e il modello con le
@@ -534,7 +558,21 @@ namespace AutoBidder.Ml
///
public static void OnAuctionClosed(CompletedAuctionRecord record)
{
- if (record == null) return;
+ if (record == null || string.IsNullOrEmpty(record.AuctionId)) return;
+
+ // Una volta per asta. La chiusura arrivava due volte (fine e rimozione dal
+ // monitor) e, al riavvio, di nuovo per le aste concluse rimaste in elenco:
+ // il modello, il profilo e il bandit le contavano due volte.
+ lock (Sync)
+ {
+ if (!_closedSeen.Add(record.AuctionId)) return;
+ }
+
+ if (AlreadyLearned(record.AuctionId))
+ {
+ OnLog?.Invoke($"[APPRENDIMENTO] '{record.Name}': già appresa, non la ristudio");
+ return;
+ }
try
{
@@ -546,8 +584,8 @@ namespace AutoBidder.Ml
// Le righe dell'asta sono in coda: prima devono essere sul disco.
Db.Flush();
- var learned = !string.IsNullOrEmpty(record.AuctionId) && IngestAuction(record.AuctionId);
- var decisioni = string.IsNullOrEmpty(record.AuctionId) ? 0 : LearnBanditFromAuction(record.AuctionId);
+ var learned = IngestAuction(record.AuctionId);
+ var decisioni = LearnBanditFromAuction(record.AuctionId);
Save();
diff --git a/Mimante/Ml/Theory.cs b/Mimante/Ml/Theory.cs
index 5e3c075..8083727 100644
--- a/Mimante/Ml/Theory.cs
+++ b/Mimante/Ml/Theory.cs
@@ -67,11 +67,13 @@ namespace AutoBidder.Ml
/// già coperto. Quindi il costo marginale nel ramo di sconfitta è c finché
/// spese + c ≤ L − V, zero dopo.
///
- /// Per i buoni Bidoo Shop con V = L la copertura è totale dal primo
- /// centesimo: ogni puntata ha valore atteso non negativo appena il margine è
- /// positivo. È un'ipotesi forte — presume che si eserciti davvero il Compralo Ora
- /// e che il buono valga davvero il suo nominale — e per questo è un'impostazione
- /// dell'utente, non un dato.
+ /// Con V = L la copertura è totale dal primo centesimo: ogni puntata ha
+ /// valore atteso non negativo appena il margine è positivo, a qualunque
+ /// probabilità — cioè si punta sempre. È un'ipotesi forte — presume che si
+ /// eserciti davvero il Compralo Ora e che il bene valga davvero il listino — e per
+ /// questo la strategia la applica solo quando l'utente ha scritto il valore reale
+ /// del prodotto (BidStrategyService.HasExplicitValue): con V lasciato al
+ /// listino la puntata si conta persa, come senza copertura.
///
public static double BidExpectedValue(in BidSituation s)
{
diff --git a/Mimante/Models/AuctionInfo.cs b/Mimante/Models/AuctionInfo.cs
index d56da70..7ad95ca 100644
--- a/Mimante/Models/AuctionInfo.cs
+++ b/Mimante/Models/AuctionInfo.cs
@@ -644,6 +644,13 @@ namespace AutoBidder.Models
[JsonIgnore]
public int LastUsedOffsetMs { get; set; } = 0;
+ ///
+ /// La chiusura è già stata comunicata a storico, statistiche e apprendimento.
+ /// Si salva con l'asta: un'asta conclusa che resta in elenco fino al riavvio non
+ /// deve essere contata di nuovo.
+ ///
+ public bool CompletionReported { get; set; }
+
///
/// Indica se questa asta � stata seguita dall'inizio (per salvare storia completa)
///
diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs
index 9cb3d43..f2fc14e 100644
--- a/Mimante/Services/AuctionMonitor.cs
+++ b/Mimante/Services/AuctionMonitor.cs
@@ -152,7 +152,7 @@ namespace AutoBidder.Services
try
{
var won = finalState.Status == AuctionStatus.EndedWon || IsAuctionWonByUser(auction);
- OnAuctionCompleted?.Invoke(auction, finalState, won);
+ RaiseCompleted(auction, finalState, won);
}
catch (Exception ex)
{
@@ -456,7 +456,7 @@ namespace AutoBidder.Services
// Il dossier registra tutto ciò che passa di qui: è materiale che esiste
// solo adesso.
var pollSettings = Settings;
- AuctionRecorder.OpenFor(auction, pollSettings)?.Poll(state, pollSettings.DossierIncludeRawPolls);
+ AuctionRecorder.OpenFor(auction, pollSettings)?.Poll(state, pollSettings.RecordPolls);
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
{
@@ -583,21 +583,6 @@ namespace AutoBidder.Services
rttMs: result?.LatencyMs ?? 0,
decisionId: auction.PendingDecisionId);
- // L'anticipo effettivo dice se il valore impostato e' quello giusto: da solo
- // nel log non serviva a nulla, sommato su molte puntate diventa una taratura.
- if (Settings.BidLeadTrackingEnabled && result != null)
- {
- BidLeadStats.Record(new BidLeadStats.Sample
- {
- At = DateTime.Now,
- AuctionId = auction.AuctionId,
- ConfiguredMs = leadMs,
- ActualMs = actualLeadMs,
- PingMs = auction.AverageLatencyMs,
- Success = result.Success,
- TooLate = !result.Success && IsLateBidError(result.Error)
- });
- }
}
/// Il server ha rifiutato perche' il tempo era gia' scaduto.
@@ -686,7 +671,7 @@ namespace AutoBidder.Services
try
{
- OnAuctionCompleted?.Invoke(auction, state, won);
+ RaiseCompleted(auction, state, won);
}
catch (Exception ex)
{
@@ -694,6 +679,25 @@ namespace AutoBidder.Services
}
}
+ private readonly object _completionGate = new();
+
+ ///
+ /// Comunica la chiusura di un'asta una volta sola. L'asta finisce (stato
+ /// terminale) e poco dopo viene tolta dall'elenco, e tutti e due i passaggi
+ /// arrivavano qui: storico, statistiche per prodotto, profilo e modello la
+ /// contavano due volte, e il bandit rivedeva gli stessi esiti.
+ ///
+ private void RaiseCompleted(AuctionInfo auction, AuctionState state, bool won)
+ {
+ lock (_completionGate)
+ {
+ if (auction.CompletionReported) return;
+ auction.CompletionReported = true;
+ }
+
+ OnAuctionCompleted?.Invoke(auction, state, won);
+ }
+
///
/// Aggiorna il valore calcolato del prodotto
///
diff --git a/Mimante/Services/BidStrategyService.cs b/Mimante/Services/BidStrategyService.cs
index ddb2956..8862df2 100644
--- a/Mimante/Services/BidStrategyService.cs
+++ b/Mimante/Services/BidStrategyService.cs
@@ -47,6 +47,23 @@ namespace AutoBidder.Services
return scheda?.MarketValue is > 0 ? scheda.MarketValue : auction.BuyNowPrice;
}
+ ///
+ /// Vero se l'utente ha scritto il valore reale del prodotto nella scheda Prodotti.
+ ///
+ /// Conta per la copertura «Compralo Ora»: senza un valore scelto, V coincide
+ /// con il listino L, la perdita coperta L − V è zero e ogni puntata risulterebbe
+ /// gratis nel ramo di sconfitta — cioè si punterebbe sempre, a qualunque
+ /// probabilità. Nelle osservazioni in shadow è successo esattamente questo: BID
+ /// con P(senza risposta) dell'1%. La copertura vale solo quando l'utente ha
+ /// detto quanto vale davvero il bene.
+ ///
+ public static bool HasExplicitValue(AuctionInfo auction)
+ {
+ if (auction == null) return false;
+ var scheda = WatchedProductsStore.FindByName(auction.Name);
+ return scheda?.MarketValue is > 0;
+ }
+
public BidDecision ShouldPlaceBid(
AuctionInfo auction,
AuctionState state,
@@ -92,6 +109,18 @@ namespace AutoBidder.Services
}
}
+ // ── Apprendimento spento: solo i limiti ────────────────────────
+ //
+ // L'interruttore in barra. Spento, da qui in poi non si decide più niente:
+ // né duello, né valore atteso, né regime, né bandit. Si punta, entro i limiti
+ // già controllati sopra, come un cecchino senza cervello.
+ if (!settings.LearningEnabled)
+ {
+ decision.Policy = "solo-limiti";
+ decision.Reason = "apprendimento spento: si punta entro i limiti (prezzo, puntate, budget, fascia oraria)";
+ return decision;
+ }
+
// ── 3. Duello con l'autopuntata ────────────────────────────────
if (auction.AutoBidDuelDetected)
{
@@ -134,7 +163,7 @@ namespace AutoBidder.Services
Shipping: auction.ShippingCost ?? 0,
ListPrice: auction.BuyNowPrice,
BidsSpentEuro: speso,
- HedgeEnabled: settings.BuyNowHedgeEnabled);
+ HedgeEnabled: settings.BuyNowHedgeEnabled && HasExplicitValue(auction));
var valore = Theory.BidExpectedValue(situazione);
diff --git a/Mimante/Services/FreeBidsPageParser.cs b/Mimante/Services/FreeBidsPageParser.cs
index 471dfd8..3c1e737 100644
--- a/Mimante/Services/FreeBidsPageParser.cs
+++ b/Mimante/Services/FreeBidsPageParser.cs
@@ -47,11 +47,18 @@ namespace AutoBidder.Services
if (html.Length < 200)
return FreeBidsPageScan.Unrecognised($"risposta troppo corta ({html.Length} caratteri)");
- if (LooksLikeLogin(html, config))
+ // Prima si guarda se è la pagina delle ricompense: anche da collegati la
+ // pagina porta un collegamento a /login.php (menu, modulo nascosto), e
+ // giudicarla «pagina di accesso» per quello dava una sessione scaduta che
+ // non c'era. Solo una pagina senza nessun segno delle ricompense, e con i
+ // segni dell'accesso, è davvero il modulo di login.
+ var markers = config.PageRecognition.PageMarkers;
+ var looksLikeRewards = markers.Count > 0 && markers.Any(m => Contains(html, m));
+
+ if (!looksLikeRewards && LooksLikeLogin(html, config))
return FreeBidsPageScan.Unrecognised("sessione scaduta: Bidoo ha risposto con la pagina di accesso");
- var markers = config.PageRecognition.PageMarkers;
- if (markers.Count > 0 && !markers.Any(m => Contains(html, m)))
+ if (markers.Count > 0 && !looksLikeRewards)
return FreeBidsPageScan.Unrecognised("pagina delle ricompense non riconosciuta (formato cambiato?)");
var patterns = PatternsFor(config);
diff --git a/Mimante/Services/RiskManager.cs b/Mimante/Services/RiskManager.cs
index 3282971..6eafb8b 100644
--- a/Mimante/Services/RiskManager.cs
+++ b/Mimante/Services/RiskManager.cs
@@ -15,10 +15,7 @@ namespace AutoBidder.Services
/// Quattro vincoli, tutti in euro o in numero, tutti dell'utente.
/// Il tetto di spesa giornaliero, lo stop-loss giornaliero (quando la perdita netta
/// del giorno supera la soglia si entra in HALT), il drawdown massimo dal picco del
- /// saldo cumulato, e il numero massimo di aste su cui puntare insieme. Sopra a tutti,
- /// il kill-switch: un file sentinella in %LocalAppData%\AutoBidder che,
- /// finché esiste, ferma ogni puntata dal vivo — lo si può creare anche da fuori,
- /// senza aprire l'applicazione.
+ /// saldo cumulato, e il numero massimo di aste su cui puntare insieme.
///
/// HALT è persistente. Si scrive nel database e sopravvive al riavvio:
/// una regola di sicurezza che si azzera chiudendo e riaprendo il programma non è una
@@ -47,45 +44,6 @@ namespace AutoBidder.Services
private static string? _haltReason;
private static bool _haltLoaded;
- private static DateTime _killSwitchCheckedAt = DateTime.MinValue;
- private static bool _killSwitchState;
-
- // ── Kill-switch ──────────────────────────────────────────────────
-
- /// Il file sentinella: finché esiste, nessuna puntata dal vivo.
- public static string KillSwitchFile => Path.Combine(AppPaths.Root, "KILL_SWITCH");
-
- /// Vero se il file c'è. Si rilegge al massimo una volta al secondo.
- public static bool KillSwitchActive
- {
- get
- {
- lock (Sync)
- {
- if ((DateTime.UtcNow - _killSwitchCheckedAt).TotalSeconds >= 1)
- {
- _killSwitchState = File.Exists(KillSwitchFile);
- _killSwitchCheckedAt = DateTime.UtcNow;
- }
- return _killSwitchState;
- }
- }
- }
-
- public static void SetKillSwitch(bool on)
- {
- lock (Sync)
- {
- try
- {
- if (on) File.WriteAllText(KillSwitchFile, $"creato il {DateTime.Now:yyyy-MM-dd HH:mm:ss}\n");
- else if (File.Exists(KillSwitchFile)) File.Delete(KillSwitchFile);
- }
- catch { }
- _killSwitchState = on;
- _killSwitchCheckedAt = DateTime.UtcNow;
- }
- }
// ── HALT ─────────────────────────────────────────────────────────
@@ -206,16 +164,13 @@ namespace AutoBidder.Services
// ── Autorizzazione ───────────────────────────────────────────────
///
- /// Può partire una puntata dal vivo su quest'asta? Controlla, nell'ordine: il
- /// kill-switch, l'HALT, lo stop-loss del giorno, il drawdown, il tetto di spesa
- /// del giorno, le aste in gioco insieme. Le prime due sono assolute; le altre
- /// valgono solo se impostate (0 = nessun limite).
+ /// Può partire una puntata dal vivo su quest'asta? Controlla, nell'ordine:
+ /// l'HALT, lo stop-loss del giorno, il drawdown, il tetto di spesa del giorno, le
+ /// aste in gioco insieme. Il primo è assoluto; gli altri valgono solo se impostati
+ /// (0 = nessun limite).
///
public static Verdict Authorize(AuctionInfo auction, AppSettings settings, IReadOnlyList? allAuctions = null)
{
- if (KillSwitchActive)
- return new Verdict(false, "kill-switch attivo: nessuna puntata dal vivo finché esiste il file KILL_SWITCH");
-
if (Halted)
return new Verdict(false, $"HALT del gestore del rischio: {HaltReason}. Si toglie dalle Impostazioni");
@@ -271,7 +226,6 @@ namespace AutoBidder.Services
_ledgerLoaded = false;
_haltLoaded = false;
_haltReason = null;
- _killSwitchCheckedAt = DateTime.MinValue;
}
}
}
diff --git a/Mimante/Tests/AuctionRecorderTests.cs b/Mimante/Tests/AuctionRecorderTests.cs
index 186e066..5d7db0d 100644
--- a/Mimante/Tests/AuctionRecorderTests.cs
+++ b/Mimante/Tests/AuctionRecorderTests.cs
@@ -21,8 +21,8 @@ public class AuctionRecorderTests
private static AppSettings Settings(bool rawPolls = true) => new()
{
- WriteAuctionDossiers = true,
- DossierIncludeRawPolls = rawPolls
+ RecordAuctions = true,
+ RecordPolls = rawPolls
};
private static AuctionInfo NewAuction() => new()
@@ -249,6 +249,6 @@ public class AuctionRecorderTests
public void Con_la_registrazione_spenta_non_si_apre_nulla()
{
var auction = NewAuction();
- Assert.Null(AuctionRecorder.OpenFor(auction, new AppSettings { WriteAuctionDossiers = false }));
+ Assert.Null(AuctionRecorder.OpenFor(auction, new AppSettings { RecordAuctions = false }));
}
}
diff --git a/Mimante/Tests/BidLeadStatsTests.cs b/Mimante/Tests/BidLeadStatsTests.cs
deleted file mode 100644
index f268e1a..0000000
--- a/Mimante/Tests/BidLeadStatsTests.cs
+++ /dev/null
@@ -1,223 +0,0 @@
-using AutoBidder.Models;
-using AutoBidder.Utilities;
-using Xunit;
-
-namespace AutoBidder.Tests;
-
-///
-/// La taratura dell'anticipo tocca il parametro che decide se una puntata arriva o no.
-/// Un consiglio troppo aggressivo si paga in aste perse, quindi la proprietà da
-/// verificare è la prudenza: mai scendere sotto il margine di rete.
-///
-public class BidLeadAdviceTests
-{
- // Le prove usano lo store reale, quindi si parte sempre da pulito.
- private static void Reset() => BidLeadStats.Clear();
-
- private static void Add(int configured, double actual, double ping = 60, bool ok = true, bool late = false)
- => BidLeadStats.Record(new BidLeadStats.Sample
- {
- AuctionId = "t",
- ConfiguredMs = configured,
- ActualMs = actual,
- PingMs = ping,
- Success = ok,
- TooLate = late
- });
-
- [Fact]
- public void Senza_dati_sufficienti_non_da_consigli()
- {
- Reset();
- for (var i = 0; i < 5; i++) Add(800, 700);
-
- var advice = BidLeadStats.Suggest(currentLeadMs: 800, minSamples: 15);
-
- Assert.False(advice.HasAdvice);
- Assert.Equal(800, advice.SuggestedLeadMs);
- }
-
- [Fact]
- public void Con_margine_ampio_propone_di_scendere()
- {
- Reset();
- // Le puntate arrivano sempre ~1700 ms prima della scadenza, ping 60 ms:
- // c'è spazio abbondante sopra il margine di sicurezza.
- for (var i = 0; i < 20; i++) Add(1800, 1700 + i % 5, ping: 60);
-
- var advice = BidLeadStats.Suggest(1800, minSamples: 15);
-
- Assert.True(advice.HasAdvice);
- Assert.True(advice.SuggestedLeadMs < 1800, "avrebbe dovuto proporre di scendere");
- Assert.True(advice.SuggestedLeadMs >= BidLeadStats.SafeFloorMs,
- "non deve scendere sotto il pavimento ricavato dai dossier");
- }
-
- [Fact]
- public void Non_scende_mai_sotto_il_pavimento_anche_con_ping_minimo()
- {
- Reset();
- // Ping bassissimo e puntate larghissime: la vecchia regola avrebbe proposto
- // un anticipo di poche centinaia di ms, dove basta un picco di rete per
- // arrivare a giochi chiusi (p99,9 del ping misurato: 444 ms).
- for (var i = 0; i < 20; i++) Add(3000, 2900, ping: 10);
-
- var advice = BidLeadStats.Suggest(3000, minSamples: 15);
-
- Assert.True(advice.SuggestedLeadMs >= BidLeadStats.SafeFloorMs,
- $"proposto {advice.SuggestedLeadMs}, sotto il pavimento di {BidLeadStats.SafeFloorMs} ms");
- }
-
- [Fact]
- public void Senza_misure_avverte_comunque_se_lanticipo_e_fuori_fascia()
- {
- Reset();
-
- // Troppo stretto: il rischio è la puntata che arriva tardi.
- var stretto = BidLeadStats.Suggest(200, minSamples: 15);
- Assert.True(stretto.HasAdvice);
- Assert.Equal(BidLeadStats.SafeFloorMs, stretto.SuggestedLeadMs);
-
- // Troppo largo: il costo sono le puntate spese in cicli che si sarebbero
- // risolti da soli.
- var largo = BidLeadStats.Suggest(4000, minSamples: 15);
- Assert.True(largo.HasAdvice);
- Assert.Equal(BidLeadStats.SafeCeilingMs, largo.SuggestedLeadMs);
-
- // Dentro la fascia, senza misure non c'è nulla da dire.
- var giusto = BidLeadStats.Suggest(1000, minSamples: 15);
- Assert.False(giusto.HasAdvice);
- }
-
- [Fact]
- public void Non_scende_mai_sotto_il_margine_di_rete()
- {
- Reset();
- // Ping alto: il margine di sicurezza è grande e va rispettato.
- for (var i = 0; i < 20; i++) Add(2000, 1900, ping: 400);
-
- var advice = BidLeadStats.Suggest(2000, minSamples: 15);
-
- if (advice.HasAdvice)
- {
- // safety = ping (400) + 150 = 550
- Assert.True(advice.SuggestedLeadMs >= 550,
- $"proposto {advice.SuggestedLeadMs}, sotto il margine di sicurezza");
- }
- }
-
- [Fact]
- public void Con_puntate_tardive_propone_di_allargare()
- {
- Reset();
- for (var i = 0; i < 18; i++) Add(300, 120, ping: 90);
- for (var i = 0; i < 3; i++) Add(300, -20, ping: 90, ok: false, late: true);
-
- var advice = BidLeadStats.Suggest(300, minSamples: 15);
-
- Assert.True(advice.HasAdvice);
- Assert.True(advice.SuggestedLeadMs > 300, "con puntate tardive deve allargare, non stringere");
- }
-
- [Fact]
- public void Se_l_anticipo_e_gia_stretto_non_propone_nulla()
- {
- Reset();
- // Anticipo 250 ms, ping 60: margine 210, quasi nessuno spazio.
- for (var i = 0; i < 20; i++) Add(250, 240, ping: 60);
-
- var advice = BidLeadStats.Suggest(250, minSamples: 15);
-
- Assert.False(advice.HasAdvice);
- }
-
- [Fact]
- public void Il_riepilogo_conta_riuscite_e_tardive()
- {
- Reset();
- for (var i = 0; i < 10; i++) Add(800, 700);
- Add(800, -5, ok: false, late: true);
-
- var s = BidLeadStats.Summarize();
-
- Assert.Equal(11, s.Count);
- Assert.Equal(10, s.Successes);
- Assert.Equal(1, s.TooLate);
-
- // La media considera solo le puntate accettate: una rifiutata non è mai arrivata.
- Assert.Equal(700, s.AverageActualMs, 1);
- }
-}
-
-///
-/// La scheda dettagliata è la materia prima per migliorare le strategie: i valori
-/// derivati devono essere corretti, o le analisi partiranno da numeri sbagliati.
-///
-public class AuctionDetailRecordTests
-{
- [Fact]
- public void La_quota_del_puntatore_principale_e_una_percentuale()
- {
- var bids = new Dictionary { ["tizio"] = 6, ["caio"] = 3, ["sempronio"] = 1 };
-
- Assert.Equal(60.0, AuctionDetailRecord.ComputeTopShare(bids), 1);
- }
-
- [Fact]
- public void Senza_puntatori_la_quota_e_zero()
- {
- Assert.Equal(0, AuctionDetailRecord.ComputeTopShare(new Dictionary()));
- }
-
- [Fact]
- public void Il_costo_reale_somma_prezzo_puntate_e_spedizione()
- {
- var r = new AuctionDetailRecord { FinalPrice = 12.50, MyBids = 40, ShippingCost = 5.90 };
-
- // 12,50 + 40 x 0,20 + 5,90 = 26,40
- Assert.Equal(26.40, r.TotalCostIfWon(0.20), 2);
- }
-
- [Fact]
- public void Il_rapporto_col_valore_prodotto_manca_se_il_valore_e_ignoto()
- {
- Assert.Null(new AuctionDetailRecord { FinalPrice = 10 }.FinalPriceRatio);
- Assert.Equal(0.1, new AuctionDetailRecord { FinalPrice = 10, BuyNowPrice = 100 }.FinalPriceRatio!.Value, 3);
- }
-}
-
-/// La serie prezzi si raccoglie solo mentre l'asta vive: va registrata bene.
-public class PriceSeriesTests
-{
- [Fact]
- public void Registra_solo_i_cambi_di_prezzo()
- {
- var a = new AuctionInfo();
-
- a.TrackPrice(1.00);
- a.TrackPrice(1.00); // identico: ignorato
- a.TrackPrice(1.02);
-
- Assert.Equal(2, a.SnapshotPriceSeries().Count);
- }
-
- [Fact]
- public void Ignora_i_prezzi_non_validi()
- {
- var a = new AuctionInfo();
- a.TrackPrice(0);
- a.TrackPrice(-1);
-
- Assert.Empty(a.SnapshotPriceSeries());
- }
-
- [Fact]
- public void Non_cresce_oltre_il_tetto()
- {
- var a = new AuctionInfo();
- for (var i = 1; i <= 1500; i++) a.TrackPrice(i * 0.01);
-
- // Il diradamento tiene la serie sotto controllo senza perderne la forma.
- Assert.True(a.SnapshotPriceSeries().Count <= 600);
- }
-}
diff --git a/Mimante/Tests/ExportAndLogTests.cs b/Mimante/Tests/ExportAndLogTests.cs
index 3eb473e..42a4ebd 100644
--- a/Mimante/Tests/ExportAndLogTests.cs
+++ b/Mimante/Tests/ExportAndLogTests.cs
@@ -13,7 +13,7 @@ namespace AutoBidder.Tests;
/// un'asta sbagliata non si nota — si notano invece le conclusioni sbagliate che se ne
/// traggono un mese dopo, quando non c'è più modo di capire da dove venivano.
///
-public class AuctionExportFilterTests
+public class AuctionDetailRecordTests
{
private static AuctionDetailRecord Record(
string id = "1",
@@ -67,145 +67,6 @@ public class AuctionExportFilterTests
Assert.Equal(0.1, withValue.FinalPriceRatio!.Value, 3);
}
- [Fact]
- public void I_filtri_predefiniti_lasciano_passare_tutto()
- {
- var filter = new AuctionExportFilter();
-
- Assert.True(filter.IncludeWon);
- Assert.True(filter.IncludeLost);
- Assert.True(filter.IncludeClosed);
- Assert.False(filter.OnlyComplete);
- Assert.Equal(0, filter.MaxAuctions);
- }
-
- [Fact]
- public void Il_percorso_predefinito_sta_nella_cartella_esportazioni()
- {
- var path = AuctionExporter.DefaultPath();
-
- Assert.StartsWith(AppPaths.ExportFolder, path);
- Assert.EndsWith(".json", path);
- }
-
- [Fact]
- public void Senza_storico_lesportazione_lo_dice_invece_di_scrivere_un_file_vuoto()
- {
- var result = AuctionExporter.Export(new AuctionExportFilter
- {
- // Un periodo in cui non può esserci nulla.
- From = new DateTime(1990, 1, 1),
- To = new DateTime(1990, 1, 2)
- });
-
- Assert.False(result.Success);
- Assert.Equal(0, result.AuctionCount);
- Assert.Contains("nessuna asta", result.Message);
- }
-}
-
-///
-/// Il registro passa da una coda: chi scrive non aspetta il disco. La proprietà da
-/// difendere è che le righe arrivino comunque nel file, e nell'ordine giusto — un registro
-/// che perde o mescola le righe è peggio di nessun registro, perché lo si legge credendogli.
-///
-public class FileLogWriterTests
-{
- private static string TempFile() =>
- Path.Combine(Path.GetTempPath(), "AutoBidder.Tests", $"log-{Guid.NewGuid():N}.log");
-
- [Fact]
- public void Quello_che_si_scrive_finisce_nel_file()
- {
- var path = TempFile();
- var writer = FileLogWriter.For(path);
-
- writer.Write("prima");
- writer.Write("seconda");
- writer.Flush();
-
- var lines = File.ReadAllLines(path);
-
- Assert.Equal(new[] { "prima", "seconda" }, lines);
-
- writer.Dispose();
- }
-
- [Fact]
- public void Lo_stesso_percorso_da_sempre_lo_stesso_scrittore()
- {
- // Due scrittori sullo stesso file mescolerebbero le righe fra loro.
- var path = TempFile();
-
- Assert.Same(FileLogWriter.For(path), FileLogWriter.For(path));
-
- FileLogWriter.For(path).Dispose();
- }
-
- [Fact]
- public void Le_righe_si_accodano_a_quelle_gia_scritte()
- {
- var path = TempFile();
-
- var first = FileLogWriter.For(path);
- first.Write("sessione 1");
- first.Flush();
- first.Dispose();
-
- var second = FileLogWriter.For(path);
- second.Write("sessione 2");
- second.Flush();
- second.Dispose();
-
- Assert.Equal(new[] { "sessione 1", "sessione 2" }, File.ReadAllLines(path));
- }
-
- [Fact]
- public void Un_percorso_impossibile_non_solleva()
- {
- // Il registro non deve mai poter interrompere un'asta.
- var writer = FileLogWriter.For(Path.Combine("Z:\\", "inesistente", "x.log"));
-
- writer.Write("qualcosa");
- writer.Flush();
- writer.Dispose();
- }
-
- [Fact]
- public void I_registri_scaduti_si_cancellano_e_quelli_recenti_no()
- {
- var folder = Path.Combine(Path.GetTempPath(), "AutoBidder.Tests", Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(folder);
-
- var old = Path.Combine(folder, "app-2020-01-01.log");
- var recent = Path.Combine(folder, "app-oggi.log");
-
- File.WriteAllText(old, "vecchio");
- File.WriteAllText(recent, "recente");
- File.SetLastWriteTime(old, DateTime.Now.AddDays(-120));
-
- var removed = FileLogWriter.PurgeOlderThan(folder, "app-*.log", days: 90);
-
- Assert.Equal(1, removed);
- Assert.False(File.Exists(old));
- Assert.True(File.Exists(recent));
- }
-
- [Fact]
- public void Con_zero_giorni_non_si_cancella_nulla()
- {
- // 0 = "tienili per sempre": interpretarlo come "cancellali tutti" distruggerebbe
- // esattamente i dati che si voleva conservare.
- var folder = Path.Combine(Path.GetTempPath(), "AutoBidder.Tests", Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(folder);
-
- var file = Path.Combine(folder, "app-vecchio.log");
- File.WriteAllText(file, "x");
- File.SetLastWriteTime(file, DateTime.Now.AddYears(-3));
-
- Assert.Equal(0, FileLogWriter.PurgeOlderThan(folder, "app-*.log", days: 0));
- Assert.True(File.Exists(file));
- }
}
///
diff --git a/Mimante/Tests/HedgeRequiresValueTests.cs b/Mimante/Tests/HedgeRequiresValueTests.cs
new file mode 100644
index 0000000..a4bc8da
--- /dev/null
+++ b/Mimante/Tests/HedgeRequiresValueTests.cs
@@ -0,0 +1,59 @@
+using System;
+using AutoBidder.Ml;
+using AutoBidder.Models;
+using AutoBidder.Services;
+using AutoBidder.Utilities;
+using Xunit;
+
+namespace AutoBidder.Tests;
+
+///
+/// La copertura «Compralo Ora» con V lasciato al listino rende gratis ogni puntata: la
+/// strategia la concede solo ai prodotti con il valore reale scritto dall'utente.
+///
+[Collection("Database")]
+public class HedgeRequiresValueTests
+{
+ [Fact]
+ public void Con_V_uguale_al_listino_la_copertura_rende_gratis_ogni_puntata()
+ {
+ // È il caso degenere: P dell'uno per mille e valore atteso comunque positivo.
+ var s = new Theory.BidSituation(PWin: 0.001, Value: 11, CurrentPrice: 1.48, BidCost: 0.20,
+ TransactionFee: 1, Shipping: 0, ListPrice: 11, BidsSpentEuro: 0, HedgeEnabled: true);
+ Assert.Equal(0, Theory.LossBranchCost(s));
+ Assert.True(Theory.BidExpectedValue(s) > 0);
+
+ // Senza copertura la stessa puntata è in perdita: P deve superare c / (guadagno + c).
+ var senza = s with { HedgeEnabled = false };
+ Assert.Equal(0.20, Theory.LossBranchCost(senza));
+ Assert.True(Theory.BidExpectedValue(senza) < 0);
+ }
+
+ [Fact]
+ public void La_copertura_vale_solo_con_il_valore_reale_scritto()
+ {
+ var nome = $"Buono di prova {Guid.NewGuid():N}";
+ var catalogo = new CatalogAuction { AuctionId = "prova-copertura", Name = nome, BuyNowPrice = 11m, ProductId = 987654 };
+ Assert.True(WatchedProductsStore.Add(catalogo));
+
+ try
+ {
+ var asta = new AuctionInfo { Name = nome, BuyNowPrice = 11 };
+
+ Assert.False(BidStrategyService.HasExplicitValue(asta));
+ Assert.Equal(11, BidStrategyService.ValueOf(asta));
+
+ var scheda = WatchedProductsStore.FindByName(nome);
+ Assert.NotNull(scheda);
+ scheda!.MarketValue = 9.5;
+ WatchedProductsStore.Persist();
+
+ Assert.True(BidStrategyService.HasExplicitValue(asta));
+ Assert.Equal(9.5, BidStrategyService.ValueOf(asta));
+ }
+ finally
+ {
+ WatchedProductsStore.Remove(catalogo);
+ }
+ }
+}
diff --git a/Mimante/Tests/LearningDoubleCloseTests.cs b/Mimante/Tests/LearningDoubleCloseTests.cs
new file mode 100644
index 0000000..90c7ded
--- /dev/null
+++ b/Mimante/Tests/LearningDoubleCloseTests.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AutoBidder.Data;
+using AutoBidder.Ml;
+using AutoBidder.Models;
+using Xunit;
+
+namespace AutoBidder.Tests;
+
+///
+/// La chiusura di un'asta arrivava due volte all'apprendimento (fine, poi rimozione dal
+/// monitor) e di nuovo al riavvio: la stessa asta va studiata una volta sola.
+///
+[Collection("Database")]
+public class LearningDoubleCloseTests
+{
+ [Fact]
+ public void Una_asta_gia_appresa_non_viene_ristudiata()
+ {
+ var db = AuctionDatabase.Instance;
+ var id = "doppia-" + Guid.NewGuid().ToString("N");
+ var now = AuctionDatabase.Now();
+ db.ExecuteNow(
+ "INSERT INTO auctions(auction_id, product_key, name, added_at, first_seen_at, opened_at, status, outcome, learned_at) " +
+ "VALUES(?1, 'prova', 'Prova doppia', ?2, ?2, ?2, 'closed', 'Persa', ?2)", id, now);
+
+ var apprese = LearningService.AuctionsLearned;
+ var profilo = LearningService.ProfileAuctions;
+ var messaggi = new List();
+ void Raccogli(string m) { lock (messaggi) messaggi.Add(m); }
+ LearningService.OnLog += Raccogli;
+
+ try
+ {
+ var record = new CompletedAuctionRecord { AuctionId = id, Name = "Prova doppia", ProductKey = "prova", EndedAt = DateTime.Now };
+ LearningService.OnAuctionClosed(record);
+ LearningService.OnAuctionClosed(record);
+ }
+ finally
+ {
+ LearningService.OnLog -= Raccogli;
+ }
+
+ Assert.Equal(apprese, LearningService.AuctionsLearned);
+ Assert.Equal(profilo, LearningService.ProfileAuctions);
+
+ // La prima chiamata vede learned_at e lo dice; la seconda si ferma prima ancora
+ // di guardare il database.
+ Assert.Equal(1, messaggi.Count(m => m.Contains("già appresa")));
+ }
+}
diff --git a/Mimante/Tests/OperationalDatabaseTests.cs b/Mimante/Tests/OperationalDatabaseTests.cs
new file mode 100644
index 0000000..81750c1
--- /dev/null
+++ b/Mimante/Tests/OperationalDatabaseTests.cs
@@ -0,0 +1,110 @@
+using System;
+using System.IO;
+using System.Linq;
+using AutoBidder.Data;
+using AutoBidder.Utilities;
+using Xunit;
+
+namespace AutoBidder.Tests;
+
+///
+/// Il database d'esercizio: documenti per chiave, importazione una tantum dei vecchi
+/// file JSON, registri che si scrivono, si rileggono, si esportano e scadono.
+///
+[Collection("Database")]
+public class OperationalDatabaseTests
+{
+ private static OperationalDatabase Db => OperationalDatabase.Instance;
+
+ [Fact]
+ public void Un_documento_si_scrive_e_si_rilegge()
+ {
+ var key = "test/" + Guid.NewGuid().ToString("N");
+ Db.SetDocument(key, "{\"a\":1}");
+ Assert.Equal("{\"a\":1}", Db.GetDocument(key));
+
+ Db.SetDocument(key, "{\"a\":2}");
+ Assert.Equal("{\"a\":2}", Db.GetDocument(key));
+
+ Db.DeleteDocument(key);
+ Db.Flush();
+ Assert.Null(Db.GetDocument(key));
+ }
+
+ [Fact]
+ public void Il_vecchio_file_si_importa_una_volta_sola()
+ {
+ var key = "test/import-" + Guid.NewGuid().ToString("N");
+ var legacy = Path.Combine(Path.GetTempPath(), $"legacy-{Guid.NewGuid():N}.json");
+ File.WriteAllText(legacy, "{\"vecchio\":true}");
+
+ try
+ {
+ Assert.Equal("{\"vecchio\":true}", Db.GetDocumentOrImport(key, legacy));
+
+ // Il file cambia, ma il documento è già nel database: comanda il database.
+ File.WriteAllText(legacy, "{\"vecchio\":false}");
+ Assert.Equal("{\"vecchio\":true}", Db.GetDocumentOrImport(key, legacy));
+ Assert.True(File.Exists(legacy)); // resta dov'è
+ }
+ finally { File.Delete(legacy); }
+ }
+
+ [Fact]
+ public void Senza_documento_ne_file_si_ha_null()
+ {
+ Assert.Null(Db.GetDocumentOrImport("test/niente-" + Guid.NewGuid().ToString("N"), Path.Combine(Path.GetTempPath(), "non-esiste.json")));
+ }
+
+ [Fact]
+ public void Il_registro_si_scrive_si_rilegge_e_si_esporta()
+ {
+ var marker = "riga-" + Guid.NewGuid().ToString("N");
+ Db.WriteLog(OperationalDatabase.AppLog, "INFO", marker);
+ Db.WriteLog(OperationalDatabase.AppLog, "WARN", marker + "-due");
+
+ var rows = Db.ReadLog(OperationalDatabase.AppLog, 10);
+ Assert.Contains(rows, r => r.Message == marker && r.Level == "INFO");
+ Assert.Equal(marker + "-due", rows[0].Message); // dalla più recente
+
+ var file = Path.Combine(Path.GetTempPath(), $"log-{Guid.NewGuid():N}.txt");
+ try
+ {
+ var n = Db.ExportLog(OperationalDatabase.AppLog, file);
+ Assert.True(n >= 2);
+ var text = File.ReadAllText(file);
+ Assert.Contains(marker, text);
+ Assert.Contains("[WARN]", text);
+ }
+ finally { File.Delete(file); }
+ }
+
+ [Fact]
+ public void Le_righe_vecchie_scadono()
+ {
+ var old = SqliteDatabase.Iso(DateTime.Now.AddDays(-200));
+ Db.ExecuteNow($"INSERT INTO {OperationalDatabase.FreeBidsLog}(ts, level, message) VALUES(?1, 'INFO', 'antica')", old);
+ Db.WriteLog(OperationalDatabase.FreeBidsLog, "INFO", "recente");
+
+ var removed = Db.PurgeLogs(90);
+
+ Assert.True(removed >= 1);
+ var rows = Db.ReadLog(OperationalDatabase.FreeBidsLog, 100);
+ Assert.DoesNotContain(rows, r => r.Message == "antica");
+ Assert.Contains(rows, r => r.Message == "recente");
+ }
+
+ [Fact]
+ public void Le_aste_nel_monitor_vivono_nel_documento()
+ {
+ PersistenceManager.SaveAuctions(new[]
+ {
+ new Models.AuctionInfo { AuctionId = "doc-1", Name = "Uno" },
+ new Models.AuctionInfo { AuctionId = "doc-2", Name = "Due" }
+ });
+
+ var back = PersistenceManager.LoadAuctions();
+ Assert.Equal(2, back.Count);
+ Assert.Contains(back, a => a.AuctionId == "doc-2" && a.Name == "Due");
+ }
+}
diff --git a/Mimante/Tests/RiskManagerTests.cs b/Mimante/Tests/RiskManagerTests.cs
index c52d43e..0e1d31f 100644
--- a/Mimante/Tests/RiskManagerTests.cs
+++ b/Mimante/Tests/RiskManagerTests.cs
@@ -24,29 +24,11 @@ public class RiskManagerTests
private static void Pulito()
{
- RiskManager.SetKillSwitch(false);
AuctionDatabase.Instance.ExecuteNow("DELETE FROM risk_ledger");
AuctionDatabase.Instance.ExecuteNow("DELETE FROM meta WHERE key = 'risk_halt'");
RiskManager.ResetForTests();
}
- [Fact]
- public void Il_kill_switch_ferma_ogni_puntata()
- {
- Pulito();
- RiskManager.SetKillSwitch(true);
- try
- {
- var v = RiskManager.Authorize(Auction("a"), new AppSettings());
- Assert.False(v.Allowed);
- Assert.Contains("kill-switch", v.Reason);
- Assert.True(File.Exists(RiskManager.KillSwitchFile));
- }
- finally { RiskManager.SetKillSwitch(false); }
-
- Assert.True(RiskManager.Authorize(Auction("a"), new AppSettings()).Allowed);
- }
-
[Fact]
public void Lo_stop_loss_porta_in_halt_e_l_halt_resta()
{
diff --git a/Mimante/Tests/StatsWipeTests.cs b/Mimante/Tests/StatsWipeTests.cs
index b1a51f5..c0ab93e 100644
--- a/Mimante/Tests/StatsWipeTests.cs
+++ b/Mimante/Tests/StatsWipeTests.cs
@@ -6,39 +6,35 @@ using Xunit;
namespace AutoBidder.Tests;
///
-/// L'azzeramento tocca solo le voci scelte, e riconosce gli archivi mensili delle
-/// versioni precedenti dal nome senza confonderli con il resto della cartella.
+/// L'azzeramento tocca solo le voci scelte: gli archivi delle versioni precedenti sono
+/// la vecchia cartella Dati, e senza voci scelte non succede nulla.
///
[Collection("Database")]
public class StatsWipeTests
{
[Fact]
- public void Toglie_gli_archivi_mensili_e_lascia_il_resto()
+ public void Toglie_la_vecchia_cartella_dei_dati_e_lascia_il_resto()
{
- var stats = AppPaths.StatsFolder;
- Directory.CreateDirectory(stats);
+ var legacy = AppPaths.LegacyDataFolder;
+ Directory.CreateDirectory(Path.Combine(legacy, "Statistiche"));
+ var vecchio = Path.Combine(legacy, "Statistiche", "aste-2020-01.jsonl");
+ File.WriteAllText(vecchio, "{}\n");
- var legacy = Path.Combine(stats, "aste-2020-01.jsonl");
- var legacyJson = Path.Combine(stats, "aste-2020-02.json");
- var altro = Path.Combine(stats, $"altro-{Guid.NewGuid():N}.json");
-
- File.WriteAllText(legacy, "{}\n");
- File.WriteAllText(legacyJson, "[]");
+ var altro = Path.Combine(AppPaths.ConfigRoot, $"altro-{Guid.NewGuid():N}.json");
File.WriteAllText(altro, "[]");
try
{
var prima = StatsWipe.Measure();
- Assert.True(prima.LegacyFiles >= 2);
+ Assert.True(prima.LegacyFiles >= 1);
var report = StatsWipe.Run(new StatsWipe.Options
{
- History = false, ProductStats = false, BidLeadMeasures = false,
- Exports = false, LegacyArchives = true, Learning = false
+ History = false, ProductStats = false, LegacyArchives = true, Learning = false
});
- Assert.False(File.Exists(legacy));
- Assert.False(File.Exists(legacyJson));
+ Assert.False(File.Exists(vecchio));
+ Assert.False(Directory.Exists(legacy));
Assert.True(File.Exists(altro));
Assert.Null(report.BackupPath);
Assert.Empty(report.Errors);
@@ -46,32 +42,14 @@ public class StatsWipeTests
}
finally
{
- foreach (var f in new[] { legacy, legacyJson, altro }) if (File.Exists(f)) File.Delete(f);
+ if (File.Exists(altro)) File.Delete(altro);
}
}
- [Fact]
- public void Svuota_le_esportazioni_senza_toccare_altro()
- {
- Directory.CreateDirectory(AppPaths.ExportFolder);
- var export = Path.Combine(AppPaths.ExportFolder, $"storico-{Guid.NewGuid():N}.csv");
- File.WriteAllText(export, "a;b\n");
-
- var (files, bytes) = StatsWipe.ClearExports();
-
- Assert.False(File.Exists(export));
- Assert.True(files >= 1);
- Assert.True(bytes >= 4);
- }
-
[Fact]
public void Senza_voci_scelte_non_fa_nulla()
{
- var o = new StatsWipe.Options
- {
- History = false, ProductStats = false, BidLeadMeasures = false,
- Exports = false, LegacyArchives = false, Learning = false
- };
+ var o = new StatsWipe.Options { History = false, ProductStats = false, LegacyArchives = false, Learning = false };
Assert.True(o.Nothing);
var report = StatsWipe.Run(o);
diff --git a/Mimante/Tests/TestEnvironment.cs b/Mimante/Tests/TestEnvironment.cs
index 7bd6855..2c34293 100644
--- a/Mimante/Tests/TestEnvironment.cs
+++ b/Mimante/Tests/TestEnvironment.cs
@@ -24,10 +24,7 @@ internal static class TestEnvironment
// leggerebbe e scriverebbe quelle vere di chi lancia la suite.
AppPaths.OverrideConfigRootForTests(sandbox);
- AppPaths.Configure(
- dataFolder: Path.Combine(sandbox, "Dati"),
- statsFolder: Path.Combine(sandbox, "Statistiche"),
- databaseFile: Path.Combine(sandbox, "autobidder.sqlite"));
+ AppPaths.Configure(databaseFolder: Path.Combine(sandbox, "Database"));
AppPaths.EnsureFolders();
}
diff --git a/Mimante/Themes/Controls.xaml b/Mimante/Themes/Controls.xaml
index 59319f9..b80f377 100644
--- a/Mimante/Themes/Controls.xaml
+++ b/Mimante/Themes/Controls.xaml
@@ -698,6 +698,40 @@
+
+
+