Ricalcolo dei prodotti dallo storico, finestra di avanzamento, operazioni sull'apprendimento, barre uguali
Nella scheda Prodotti c'è il pulsante che rilegge tutto lo storico e rifà da capo i numeri di ogni articolo: aste viste, prezzo minimo, massimo e medio, puntate del vincitore, vittorie, osservazioni. I limiti scritti a mano e la stellina non cambiano. Il ricalcolo si costruisce da parte e sostituisce le schede solo alla fine: un annullamento a metà lascia quelle di prima. Ogni operazione massiva passa da una finestra di avanzamento (ProgressDialog): titolo, a che punto si è, barra, conto fatto/totale, tempo passato e Annulla. Modale quando è questione di secondi, altrimenti si continua a usare l'applicazione: il recupero delle puntate dei vincitori e lo studio delle aste vanno piano di proposito. La scheda Apprendimento ha un menu di operazioni: studiare le aste chiuse non ancora apprese (lo studio all'avvio si ferma dopo qualche minuto, questo va in fondo), ricostruire il profilo per prodotto e fascia oraria, rifare il bandit dalle decisioni registrate, azzerare lo sfidante o la calibrazione, ricominciare da capo. Lo studio delle aste è una sola routine con budget di tempo facoltativo, avanzamento e annullamento; quello che è già studiato resta studiato. Le barre in alto di tutte le schede hanno la stessa altezza (stile TabToolbar): passando da una scheda all'altra i pulsanti restano allo stesso posto. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══════════════ BARRA STRUMENTI ═══════════════ -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
||||
@@ -148,8 +148,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ Barra strumenti ═══ -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ Barra strumenti ═══ -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ Barra strumenti ═══ -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -87,6 +86,41 @@
|
||||
|
||||
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="6,4"/>
|
||||
|
||||
<ToggleButton x:Name="OptionsButton" Style="{StaticResource IconToggle}"
|
||||
ToolTip="Altre operazioni sull'apprendimento. Studiare le aste non ancora apprese, ricostruire il profilo, rifare il bandit, azzerare lo sfidante o la calibrazione. Ognuna mostra l'avanzamento e si può annullare.">
|
||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||
</ToggleButton>
|
||||
<Popup x:Name="OptionsPopup" PlacementTarget="{Binding ElementName=OptionsButton}" Placement="Bottom"
|
||||
StaysOpen="False" AllowsTransparency="True" HorizontalOffset="-300" VerticalOffset="4"
|
||||
IsOpen="{Binding IsChecked, ElementName=OptionsButton, Mode=TwoWay}">
|
||||
<Border Background="{DynamicResource Brush.Surface}" BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1" CornerRadius="10" Padding="6" Width="340">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource Hint}" Margin="10,4,10,6" Text="Operazioni sull'apprendimento"/>
|
||||
<Button Style="{StaticResource MenuEntry}" Content="Studia le aste chiuse non ancora apprese"
|
||||
ToolTip="Lo studio automatico all'avvio si ferma dopo qualche minuto: questo va in fondo a tutte le aste chiuse del database che il modello non ha ancora visto. Non tocca ciò che è già appreso."
|
||||
Click="LearnPending_Click"/>
|
||||
<Button Style="{StaticResource MenuEntry}" Content="Ricostruisci il profilo per prodotto e fascia oraria"
|
||||
ToolTip="Rifà da tutto lo storico le puntate tipiche del vincitore e il rapporto di chiusura per prodotto, fascia oraria e giorno. Modello e bandit non si toccano."
|
||||
Click="RebuildProfile_Click"/>
|
||||
<Button Style="{StaticResource MenuEntry}" Content="Rifai il bandit dalle decisioni registrate"
|
||||
ToolTip="Butta il bandit Thompson e lo rifà da tutte le decisioni con esito registrate nel database: le stesse osservazioni che avrebbe raccolto asta per asta, senza doppioni."
|
||||
Click="RebuildBandit_Click"/>
|
||||
<Button Style="{StaticResource MenuEntry}" Content="Azzera lo sfidante (rete neurale)"
|
||||
ToolTip="La rete riparte da zero e il confronto Brier ricomincia. Il campione logistico resta com'è: è lui che risponde al motore finché lo sfidante non lo batte."
|
||||
Click="ResetChallenger_Click"/>
|
||||
<Button Style="{StaticResource MenuEntry}" Content="Azzera la calibrazione del modello"
|
||||
ToolTip="Il fattore osservato/previsto torna a 1 e si rimisura sugli esempi nuovi. Utile se il mercato è cambiato di colpo e la scala delle probabilità è rimasta indietro."
|
||||
Click="ResetCalibration_Click"/>
|
||||
<Border Height="1" Background="{DynamicResource Brush.Border}" Margin="6,6"/>
|
||||
<Button Style="{StaticResource MenuEntry}" Content="Ricomincia da capo: azzera tutto e ristudia lo storico"
|
||||
Foreground="{DynamicResource Brush.Danger}"
|
||||
ToolTip="Butta modello, profilo, sfidante, bandit e contatore delle aste studiate, e ristudia tutte le aste chiuse del database. Non tocca lo storico né i prodotti. Chiede conferma."
|
||||
Click="RetrainButton_Click"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
<Button x:Name="RetrainButton" Style="{StaticResource IconButton}"
|
||||
Foreground="{DynamicResource Brush.Danger}"
|
||||
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."
|
||||
|
||||
@@ -352,11 +352,78 @@ namespace AutoBidder.Controls
|
||||
}
|
||||
}
|
||||
|
||||
// ── Operazioni sull'apprendimento: tutte con la finestra di avanzamento ──
|
||||
|
||||
private Window? Host => Window.GetWindow(this);
|
||||
|
||||
private void CloseOptions() => OptionsButton.IsChecked = false;
|
||||
|
||||
/// <summary>Dice com'è finita un'operazione: errore, annullata, o il testo di riepilogo.</summary>
|
||||
private void Conclude<T>(ProgressOutcome<T> esito, string titolo, Func<T, string> riepilogo)
|
||||
{
|
||||
Refresh();
|
||||
|
||||
if (esito.Error != null)
|
||||
MessageBox.Show(Host!, esito.Error.Message, titolo, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
else if (esito.Cancelled)
|
||||
MessageBox.Show(Host!, "Operazione annullata. Quello che era già fatto resta fatto.", titolo, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
else
|
||||
MessageBox.Show(Host!, riepilogo(esito.Result!), titolo, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async void LearnPending_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseOptions();
|
||||
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Studio delle aste non ancora apprese",
|
||||
"Cerco le aste chiuse che il modello non ha ancora visto…",
|
||||
(p, ct) => Task.Run(() => LearningService.LearnPendingAsync(p, ct), ct), modal: false);
|
||||
Conclude(esito, "Apprendimento", r => $"Studiate {r.Done:N0} aste, {r.Usable:N0} utilizzabili per il modello.");
|
||||
}
|
||||
|
||||
private async void RebuildProfile_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseOptions();
|
||||
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Profilo per prodotto e fascia oraria",
|
||||
"Rileggo lo storico…",
|
||||
(p, ct) => Task.Run(() => LearningService.RebuildProfileFromHistory(p, ct), ct));
|
||||
Conclude(esito, "Profilo", n => $"Profilo ricostruito da {n:N0} aste dello storico.");
|
||||
}
|
||||
|
||||
private async void RebuildBandit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseOptions();
|
||||
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Bandit dalle decisioni registrate",
|
||||
"Leggo le decisioni con esito…",
|
||||
(p, ct) => LearningService.RebuildBanditFromDecisionsAsync(p, ct));
|
||||
Conclude(esito, "Bandit", r => $"Bandit rifatto da {r.Auctions:N0} aste: {r.Observations:N0} osservazioni.");
|
||||
}
|
||||
|
||||
private void ResetChallenger_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseOptions();
|
||||
var answer = MessageBox.Show(Host!, "Azzero lo sfidante (la rete) e il confronto Brier? Il campione logistico resta.",
|
||||
"Sfidante", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (answer != MessageBoxResult.Yes) return;
|
||||
LearningService.ResetChallenger();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void ResetCalibration_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseOptions();
|
||||
var answer = MessageBox.Show(Host!, "Azzero la calibrazione? Il fattore torna a 1 e si rimisura sugli esempi nuovi.",
|
||||
"Calibrazione", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (answer != MessageBoxResult.Yes) return;
|
||||
LearningService.ResetCalibration();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private async void RetrainButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var answer = MessageBox.Show(
|
||||
CloseOptions();
|
||||
var answer = MessageBox.Show(Host!,
|
||||
"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?",
|
||||
"Lo storico e i prodotti non vengono toccati. Puoi continuare a usare l'applicazione; la finestra mostra a che punto è.\n\nProcedo?",
|
||||
"Ricomincia da capo", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
|
||||
if (answer != MessageBoxResult.Yes) return;
|
||||
@@ -364,12 +431,14 @@ namespace AutoBidder.Controls
|
||||
RetrainButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
await LearningService.RetrainFromScratchAsync(SettingsManager.Load());
|
||||
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Ricomincio da capo",
|
||||
"Azzero modello, profilo, sfidante e bandit…",
|
||||
(p, ct) => Task.Run(() => LearningService.RetrainFromScratchAsync(p, ct), ct), modal: false);
|
||||
Conclude(esito, "Ricomincia da capo", r => $"Ristudiate {r.Done:N0} aste, {r.Usable:N0} utilizzabili per il modello.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
RetrainButton.IsEnabled = true;
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,8 +121,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ Barra strumenti ═══ -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -201,6 +200,11 @@
|
||||
|
||||
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="6,4"/>
|
||||
|
||||
<Button x:Name="RecalculateButton" Style="{StaticResource IconButton}"
|
||||
ToolTip="Ricalcola dallo storico. Rilegge tutte le aste concluse dello storico e rifà da capo i numeri di ogni prodotto in elenco: aste viste, prezzo minimo, massimo e medio, puntate del vincitore, vittorie, osservazioni. I limiti scritti a mano e la stellina non cambiano; cambiano i numeri, e quindi i limiti consigliati. Mostra l'avanzamento e si può annullare."
|
||||
Click="RecalculateButton_Click">
|
||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||
</Button>
|
||||
<Button x:Name="ApplySuggestedButton" Style="{StaticResource IconButton}"
|
||||
ToolTip="Usa i limiti consigliati sul prodotto selezionato. Scrive prezzo minimo e massimo consigliati dallo storico: il massimo copre l'85% delle chiusure passate (regolabile in Impostazioni), abbassato se il budget non lo regge — valore del prodotto meno il costo delle puntate massime. Il minimo è il decimo percentile: sotto quel prezzo l'asta non si chiude quasi mai."
|
||||
Click="ApplySuggestedButton_Click">
|
||||
|
||||
@@ -138,6 +138,9 @@ namespace AutoBidder.Controls
|
||||
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
|
||||
=> RaiseEvent(new RoutedEventArgs(ClearAllClickedEvent, this));
|
||||
|
||||
private void RecalculateButton_Click(object sender, RoutedEventArgs e)
|
||||
=> RaiseEvent(new RoutedEventArgs(RecalculateClickedEvent, this));
|
||||
|
||||
private void UnwatchAllButton_Click(object sender, RoutedEventArgs e)
|
||||
=> RaiseEvent(new RoutedEventArgs(UnwatchAllClickedEvent, this));
|
||||
|
||||
@@ -170,6 +173,9 @@ namespace AutoBidder.Controls
|
||||
public static readonly RoutedEvent ClearAllClickedEvent = EventManager.RegisterRoutedEvent(
|
||||
"ClearAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||
|
||||
public static readonly RoutedEvent RecalculateClickedEvent = EventManager.RegisterRoutedEvent(
|
||||
"RecalculateClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||
|
||||
public static readonly RoutedEvent UnwatchAllClickedEvent = EventManager.RegisterRoutedEvent(
|
||||
"UnwatchAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||
|
||||
@@ -224,6 +230,12 @@ namespace AutoBidder.Controls
|
||||
remove { RemoveHandler(ClearAllClickedEvent, value); }
|
||||
}
|
||||
|
||||
public event RoutedEventHandler RecalculateClicked
|
||||
{
|
||||
add { AddHandler(RecalculateClickedEvent, value); }
|
||||
remove { RemoveHandler(RecalculateClickedEvent, value); }
|
||||
}
|
||||
|
||||
public event RoutedEventHandler UnwatchAllClicked
|
||||
{
|
||||
add { AddHandler(UnwatchAllClickedEvent, value); }
|
||||
|
||||
@@ -217,6 +217,47 @@ namespace AutoBidder
|
||||
Log("[PRODOTTI] Elenco svuotato", LogLevel.Info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rifà i numeri di ogni scheda prodotto dallo storico, con la finestra di
|
||||
/// avanzamento. Vedi <see cref="ProductStatsStore.RebuildFromHistory"/>.
|
||||
/// </summary>
|
||||
private async void Products_RecalculateClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var aste = CompletedAuctionsStore.LoadAll().Count;
|
||||
if (aste == 0)
|
||||
{
|
||||
MessageBox.Show(this, "Lo storico è vuoto: non c'è niente da cui ricalcolare.",
|
||||
"Ricalcola dallo storico", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var answer = MessageBox.Show(this,
|
||||
$"Rileggo tutto lo storico ({aste:N0} aste concluse) e ricalcolo per ogni prodotto: aste viste, " +
|
||||
"prezzo minimo, massimo e medio, puntate del vincitore, vittorie e osservazioni.\n\n" +
|
||||
"I limiti scritti a mano e la stellina non cambiano; cambiano i numeri, e quindi i limiti consigliati.\n\nProcedo?",
|
||||
"Ricalcola dallo storico", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
|
||||
if (answer != MessageBoxResult.Yes) return;
|
||||
|
||||
var esito = await Dialogs.ProgressDialog.RunAsync(this, "Ricalcolo dallo storico", "Leggo lo storico…",
|
||||
(progress, ct) => System.Threading.Tasks.Task.Run(() => ProductStatsStore.RebuildFromHistory(progress, ct), ct));
|
||||
|
||||
if (esito.Error != null)
|
||||
{
|
||||
Log($"[PRODOTTI] Ricalcolo non riuscito: {esito.Error.Message}", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (esito.Cancelled)
|
||||
{
|
||||
Log("[PRODOTTI] Ricalcolo annullato: le schede restano come prima", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
LoadProducts();
|
||||
Log($"[PRODOTTI] Statistiche per prodotto ricalcolate dallo storico ({esito.Result:N0} aste)", LogLevel.Success);
|
||||
}
|
||||
|
||||
private void Products_ClearAllClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var answer = MessageBox.Show(this,
|
||||
|
||||
@@ -273,20 +273,44 @@ namespace AutoBidder
|
||||
var backfill = new Services.WinnerBidsBackfill(transport);
|
||||
backfill.OnLog += m => Dispatcher.Invoke(() => Log(m, LogLevel.Warning));
|
||||
|
||||
var lastReported = 0;
|
||||
backfill.OnProgress += (done, total, updated) =>
|
||||
{
|
||||
// Una riga di registro ogni cinquanta: una per asta seppellirebbe
|
||||
// tutto il resto sotto migliaia di righe.
|
||||
if (done - lastReported < 50 && done != total) return;
|
||||
lastReported = done;
|
||||
Dispatcher.Invoke(() =>
|
||||
Log($"[RECUPERO] {done}/{total} aste esaminate, {updated} aggiornate", LogLevel.Info));
|
||||
};
|
||||
|
||||
Log($"[RECUPERO] Avvio: {missing.Count} aste da completare", LogLevel.Info);
|
||||
|
||||
var result = await backfill.RunAsync(0, System.Threading.CancellationToken.None);
|
||||
// La finestra di avanzamento non blocca l'applicazione: il recupero va
|
||||
// piano di proposito e intanto si può fare altro.
|
||||
var esito = await Dialogs.ProgressDialog.RunAsync(this, "Recupero delle puntate dei vincitori",
|
||||
$"{missing.Count:N0} aste da completare, una richiesta alla volta in corsia di fondo.",
|
||||
(progress, ct) =>
|
||||
{
|
||||
var lastReported = 0;
|
||||
backfill.OnProgress += (done, total, updated) =>
|
||||
{
|
||||
progress.Report(new ProgressStep(done, total, $"{updated:N0} aggiornate"));
|
||||
|
||||
// Una riga di registro ogni cinquanta: una per asta seppellirebbe
|
||||
// tutto il resto sotto migliaia di righe.
|
||||
if (done - lastReported < 50 && done != total) return;
|
||||
lastReported = done;
|
||||
Dispatcher.Invoke(() =>
|
||||
Log($"[RECUPERO] {done}/{total} aste esaminate, {updated} aggiornate", LogLevel.Info));
|
||||
};
|
||||
return backfill.RunAsync(0, ct);
|
||||
}, modal: false);
|
||||
|
||||
if (esito.Error != null)
|
||||
{
|
||||
Log($"[RECUPERO] Non riuscito: {esito.Error.Message}", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (esito.Cancelled)
|
||||
{
|
||||
LoadStatistics();
|
||||
LoadProducts();
|
||||
Log("[RECUPERO] Interrotto dall'utente: le aste già completate restano completate", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = esito.Result!;
|
||||
|
||||
LoadStatistics();
|
||||
LoadProducts();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<Window x:Class="AutoBidder.Dialogs.ProgressDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Operazione in corso"
|
||||
Width="480" SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ShowInTaskbar="False" ResizeMode="NoResize"
|
||||
Background="{DynamicResource Brush.Bg}">
|
||||
|
||||
<StackPanel Margin="22,18">
|
||||
|
||||
<TextBlock x:Name="TitleText"
|
||||
FontSize="{StaticResource Font.Size.Lg}" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Brush.Text}"/>
|
||||
|
||||
<TextBlock x:Name="MessageText" Style="{StaticResource Hint}" Margin="0,6,0,14"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<ProgressBar x:Name="Bar" Height="6" Minimum="0" Maximum="100" IsIndeterminate="True"/>
|
||||
|
||||
<Grid Margin="0,10,0,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="CountText" Style="{StaticResource Hint}" Text=""/>
|
||||
<TextBlock x:Name="ElapsedText" Style="{StaticResource Hint}" Margin="12,0,0,0" Text=""/>
|
||||
</StackPanel>
|
||||
<Button x:Name="CancelButton" Content="Annulla" HorizontalAlignment="Right"
|
||||
Style="{StaticResource SmallRoundedButton}"
|
||||
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||
Foreground="{DynamicResource Brush.Text}"
|
||||
Padding="18,5" Click="CancelButton_Click"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using AutoBidder.Utilities;
|
||||
|
||||
namespace AutoBidder.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// La finestra di avanzamento delle operazioni lunghe: titolo, una riga che dice a
|
||||
/// che punto si è, la barra, il conto fatto/totale, il tempo passato e Annulla.
|
||||
///
|
||||
/// <para>Si usa da <see cref="RunAsync{T}"/>: si passa il lavoro da fare, che riceve
|
||||
/// un <see cref="IProgress{T}"/> e un token di annullamento; la finestra si apre, segue
|
||||
/// l'avanzamento e si chiude da sola. In modalità «modale» la finestra principale
|
||||
/// resta disabilitata finché il lavoro non finisce; altrimenti si può continuare a
|
||||
/// usare l'applicazione (le aste in corso non si fermano mai, in nessun caso).</para>
|
||||
/// </summary>
|
||||
public partial class ProgressDialog : Window
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Stopwatch _elapsed = Stopwatch.StartNew();
|
||||
private readonly DispatcherTimer _clock;
|
||||
private bool _finished;
|
||||
|
||||
public CancellationToken Token => _cts.Token;
|
||||
|
||||
public ProgressDialog(string title, string message, bool cancellable)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = title;
|
||||
TitleText.Text = title;
|
||||
MessageText.Text = message;
|
||||
CancelButton.Visibility = cancellable ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
_clock = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_clock.Tick += (_, _) => ElapsedText.Text = _elapsed.Elapsed.TotalSeconds < 60
|
||||
? $"{_elapsed.Elapsed.TotalSeconds:N0} s"
|
||||
: $"{(int)_elapsed.Elapsed.TotalMinutes} min {_elapsed.Elapsed.Seconds:00} s";
|
||||
_clock.Start();
|
||||
}
|
||||
|
||||
/// <summary>Aggiorna barra e testi. Va chiamato sul thread dell'interfaccia.</summary>
|
||||
public void Report(ProgressStep step)
|
||||
{
|
||||
if (_finished) return;
|
||||
|
||||
if (step.Total > 0)
|
||||
{
|
||||
Bar.IsIndeterminate = false;
|
||||
Bar.Maximum = step.Total;
|
||||
Bar.Value = Math.Clamp(step.Done, 0, step.Total);
|
||||
CountText.Text = $"{step.Done:N0} / {step.Total:N0}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Bar.IsIndeterminate = true;
|
||||
CountText.Text = step.Done > 0 ? step.Done.ToString("N0") : "";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(step.Message)) MessageText.Text = step.Message;
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e) => RequestCancel();
|
||||
|
||||
private void RequestCancel()
|
||||
{
|
||||
if (_cts.IsCancellationRequested) return;
|
||||
_cts.Cancel();
|
||||
CancelButton.IsEnabled = false;
|
||||
MessageText.Text = "Annullamento in corso: aspetto che il passo in corso finisca…";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La X della finestra non chiude niente a metà: chiede l'annullamento, se
|
||||
/// l'operazione lo ammette, e la finestra se ne va quando il lavoro è finito.
|
||||
/// </summary>
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
if (!_finished)
|
||||
{
|
||||
e.Cancel = true;
|
||||
if (CancelButton.Visibility == Visibility.Visible) RequestCancel();
|
||||
}
|
||||
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
_finished = true;
|
||||
_clock.Stop();
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esegue <paramref name="work"/> mostrando la finestra di avanzamento sopra
|
||||
/// <paramref name="owner"/>. Restituisce l'esito: risultato, annullata, o errore.
|
||||
/// </summary>
|
||||
/// <param name="modal">Vero: la finestra principale è disabilitata finché non si finisce.</param>
|
||||
/// <param name="cancellable">Vero: c'è il pulsante Annulla e il token viene annullato.</param>
|
||||
public static async Task<ProgressOutcome<T>> RunAsync<T>(
|
||||
Window? owner, string title, string message,
|
||||
Func<IProgress<ProgressStep>, CancellationToken, Task<T>> work,
|
||||
bool modal = true, bool cancellable = true)
|
||||
{
|
||||
var dialog = new ProgressDialog(title, message, cancellable);
|
||||
if (owner != null && owner.IsLoaded) dialog.Owner = owner;
|
||||
|
||||
// Progress<T> cattura il contesto di sincronizzazione di chi lo crea: da qui,
|
||||
// il thread dell'interfaccia. I rapporti arrivano già sul thread giusto.
|
||||
var progress = new Progress<ProgressStep>(dialog.Report);
|
||||
|
||||
if (modal && owner != null) owner.IsEnabled = false;
|
||||
dialog.Show();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await work(progress, dialog.Token);
|
||||
return dialog.Token.IsCancellationRequested
|
||||
? ProgressOutcome<T>.CancelledByUser()
|
||||
: ProgressOutcome<T>.Ok(result);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return ProgressOutcome<T>.CancelledByUser();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ProgressOutcome<T>.Failed(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (modal && owner != null) owner.IsEnabled = true;
|
||||
dialog.Finish();
|
||||
owner?.Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,7 @@
|
||||
ClearAllClicked="Products_ClearAllClicked"
|
||||
UnwatchAllClicked="Products_UnwatchAllClicked"
|
||||
RemoveUnwatchedClicked="Products_RemoveUnwatchedClicked"
|
||||
RecalculateClicked="Products_RecalculateClicked"
|
||||
ApplySuggestedClicked="Products_ApplySuggestedClicked"
|
||||
ApplyAllSuggestedClicked="Products_ApplyAllSuggestedClicked"
|
||||
ProductLimitsEdited="Products_ProductLimitsEdited"
|
||||
@@ -222,8 +223,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Barra strumenti -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
||||
@@ -234,18 +234,14 @@ namespace AutoBidder
|
||||
/// </summary>
|
||||
private void EnsureProductCardsBuilt()
|
||||
{
|
||||
const string Marker = "products-generation";
|
||||
const string Generation = "2";
|
||||
|
||||
try
|
||||
{
|
||||
var op = Data.OperationalDatabase.Instance;
|
||||
if (ProductStatsStore.GetAll().Count > 0 && op.GetDocument(Marker) == Generation) return;
|
||||
if (ProductStatsStore.GetAll().Count > 0 && op.GetDocument(ProductStatsStore.GenerationDocument) == ProductStatsStore.CurrentGeneration) return;
|
||||
|
||||
_ = System.Threading.Tasks.Task.Run(() =>
|
||||
{
|
||||
var count = ProductStatsStore.RebuildFromHistory();
|
||||
op.SetDocument(Marker, Generation);
|
||||
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
Log($"[PRODOTTI] Schede prodotto ricostruite dallo storico ({count} aste)", LogLevel.Info));
|
||||
|
||||
@@ -233,7 +233,9 @@ 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. Barra del **monitor**: l'interruttore
|
||||
campione/sfidante, bandit), simulatore; dal menu delle operazioni: studiare le aste non
|
||||
ancora apprese, ricostruire il profilo, rifare il bandit dalle decisioni, azzerare sfidante
|
||||
o calibrazione, ricominciare da capo — tutte con la finestra di avanzamento. 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
|
||||
|
||||
+136
-18
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AutoBidder.Data;
|
||||
using AutoBidder.Models;
|
||||
@@ -220,16 +221,101 @@ namespace AutoBidder.Ml
|
||||
/// studiarle dal database. Serve quando si cambiano le variabili o si sospetta che
|
||||
/// il modello abbia imparato da dati sbagliati.
|
||||
/// </summary>
|
||||
public static Task RetrainFromScratchAsync(AppSettings settings)
|
||||
public static Task<(int Done, int Usable)> RetrainFromScratchAsync(IProgress<ProgressStep>? progress = null, CancellationToken ct = default)
|
||||
{
|
||||
if (_bootstrapRunning) throw new InvalidOperationException("uno studio delle aste è già in corso: aspetta che finisca");
|
||||
|
||||
Forget();
|
||||
|
||||
OnLog?.Invoke("[APPRENDIMENTO] Ricomincio da capo: profilo dallo storico, poi tutte le aste chiuse.");
|
||||
progress?.Report(new ProgressStep(0, 0, "profilo per prodotto e fascia oraria dallo storico…"));
|
||||
BootstrapProfileFromHistory();
|
||||
|
||||
// Un riaddestramento voluto ha diritto a tutto il tempo che serve.
|
||||
var generose = new AppSettings { LearningBootstrapSecondsPerStart = Math.Max(600, settings.LearningBootstrapSecondsPerStart) };
|
||||
return Task.Run(() => BootstrapAsync(generose));
|
||||
return StudyPendingAsync(null, progress, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Studia adesso tutte le aste chiuse non ancora apprese, senza limite di tempo,
|
||||
/// riferendo l'avanzamento. È l'operazione «Studia le aste non ancora apprese»
|
||||
/// della scheda Apprendimento: lo studio automatico all'avvio si ferma dopo
|
||||
/// qualche minuto, questo va in fondo.
|
||||
/// </summary>
|
||||
public static Task<(int Done, int Usable)> LearnPendingAsync(IProgress<ProgressStep>? progress, CancellationToken ct)
|
||||
=> StudyPendingAsync(null, progress, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Ricostruisce il profilo per prodotto e fascia oraria da tutto lo storico,
|
||||
/// anche se ne esiste già uno. Il modello e il bandit non si toccano.
|
||||
/// </summary>
|
||||
public static int RebuildProfileFromHistory(IProgress<ProgressStep>? progress = null, CancellationToken ct = default)
|
||||
{
|
||||
var records = CompletedAuctionsStore.LoadAll();
|
||||
var fresh = new AuctionProfileStats();
|
||||
var done = 0;
|
||||
|
||||
foreach (var r in records)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
fresh.Observe(r.ProductKey, r.EndedAt.ToLocalTime(),
|
||||
AuctionIntegrity.TrustedWinnerBids(r), Ratio(r.FinalPrice, r.BuyNowPrice));
|
||||
if (++done % 50 == 0 || done == records.Count) progress?.Report(new ProgressStep(done, records.Count));
|
||||
}
|
||||
|
||||
lock (Sync) _profile = fresh;
|
||||
Save();
|
||||
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] Profilo delle aste ricostruito dallo storico: {records.Count:N0} aste");
|
||||
return records.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Butta il bandit e lo rifà da tutte le decisioni registrate che hanno un esito:
|
||||
/// stesse osservazioni che avrebbe raccolto asta per asta, senza doppioni.
|
||||
/// </summary>
|
||||
public static Task<(int Auctions, long Observations)> RebuildBanditFromDecisionsAsync(IProgress<ProgressStep>? progress, CancellationToken ct) => Task.Run(() =>
|
||||
{
|
||||
Db.Flush();
|
||||
var ids = Db.Query("SELECT DISTINCT auction_id FROM bot_decisions WHERE context_key IS NOT NULL AND outcome IS NOT NULL ORDER BY auction_id")
|
||||
.Select(r => r.Str("auction_id")).ToList();
|
||||
|
||||
var fresh = new ThompsonBandit();
|
||||
var done = 0;
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
LearnBanditFromAuction(fresh, id);
|
||||
if (++done % 20 == 0 || done == ids.Count)
|
||||
progress?.Report(new ProgressStep(done, ids.Count, $"{fresh.Observations:N0} osservazioni"));
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
lock (Sync) _bandit = fresh;
|
||||
Save();
|
||||
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] Bandit rifatto dalle decisioni registrate: {ids.Count:N0} aste, {fresh.Observations:N0} osservazioni");
|
||||
return (ids.Count, fresh.Observations);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Azzera lo sfidante (la rete) e il confronto Brier: il campione resta.</summary>
|
||||
public static void ResetChallenger()
|
||||
{
|
||||
lock (Sync)
|
||||
{
|
||||
_challenger = NewChallenger();
|
||||
_brierLogit = double.NaN; _brierNet = double.NaN; _brierSamples = 0;
|
||||
}
|
||||
Save();
|
||||
OnLog?.Invoke("[APPRENDIMENTO] Sfidante azzerato: la rete riparte da zero e il confronto Brier ricomincia");
|
||||
}
|
||||
|
||||
/// <summary>Azzera la calibrazione del campione: il fattore torna a 1 finché non si rimisura.</summary>
|
||||
public static void ResetCalibration()
|
||||
{
|
||||
lock (Sync) _model.ResetCalibration();
|
||||
Save();
|
||||
OnLog?.Invoke("[APPRENDIMENTO] Calibrazione azzerata: il fattore torna a 1 finché non si rimisura sugli esempi nuovi");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -413,17 +499,33 @@ namespace AutoBidder.Ml
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Studia le aste chiuse non ancora apprese, dalla più recente, finché c'è tempo.
|
||||
/// All'avvio: studia le aste chiuse non ancora apprese, dalla più recente, finché
|
||||
/// c'è tempo. Il resto aspetta il prossimo avvio, o l'operazione a mano.
|
||||
/// </summary>
|
||||
private static async Task BootstrapAsync(AppSettings settings)
|
||||
{
|
||||
if (_bootstrapRunning) return;
|
||||
await Task.Delay(3000).ConfigureAwait(false); // prima l'avvio, poi lo studio
|
||||
|
||||
var budget = TimeSpan.FromSeconds(Math.Max(10, settings.LearningBootstrapSecondsPerStart));
|
||||
try { await StudyPendingAsync(budget, null, CancellationToken.None).ConfigureAwait(false); }
|
||||
catch { /* già riferito nel registro */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Studia le aste chiuse non ancora apprese, dalla più recente. Con un budget di
|
||||
/// tempo si ferma allo scadere; senza, va in fondo. Riferisce l'avanzamento e
|
||||
/// rispetta l'annullamento: ciò che è già studiato resta studiato.
|
||||
/// </summary>
|
||||
private static async Task<(int Done, int Usable)> StudyPendingAsync(TimeSpan? budget, IProgress<ProgressStep>? progress, CancellationToken ct)
|
||||
{
|
||||
if (_bootstrapRunning) throw new InvalidOperationException("uno studio delle aste è già in corso: aspetta che finisca");
|
||||
_bootstrapRunning = true;
|
||||
|
||||
var done = 0;
|
||||
var usable = 0;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(3000).ConfigureAwait(false); // prima l'avvio, poi lo studio
|
||||
|
||||
List<string> pending;
|
||||
try
|
||||
{
|
||||
@@ -434,21 +536,26 @@ namespace AutoBidder.Ml
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] Elenco delle aste da studiare non disponibile: {ex.Message}");
|
||||
return;
|
||||
throw;
|
||||
}
|
||||
|
||||
if (pending.Count == 0) return;
|
||||
if (pending.Count == 0)
|
||||
{
|
||||
progress?.Report(new ProgressStep(0, 0, "nessuna asta da studiare: l'archivio è già tutto appreso"));
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
var budget = TimeSpan.FromSeconds(Math.Max(10, settings.LearningBootstrapSecondsPerStart));
|
||||
var sw = Stopwatch.StartNew();
|
||||
var done = 0;
|
||||
var usable = 0;
|
||||
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] {pending.Count:N0} aste da studiare, al massimo {budget.TotalSeconds:N0} s per questo avvio");
|
||||
OnLog?.Invoke(budget is { } b
|
||||
? $"[APPRENDIMENTO] {pending.Count:N0} aste da studiare, al massimo {b.TotalSeconds:N0} s per questo avvio"
|
||||
: $"[APPRENDIMENTO] {pending.Count:N0} aste da studiare");
|
||||
progress?.Report(new ProgressStep(0, pending.Count, $"{pending.Count:N0} aste da studiare"));
|
||||
|
||||
foreach (var id in pending)
|
||||
{
|
||||
if (sw.Elapsed > budget) break;
|
||||
if (budget is { } limit && sw.Elapsed > limit) break;
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (IngestAuction(id)) usable++;
|
||||
done++;
|
||||
@@ -459,6 +566,9 @@ namespace AutoBidder.Ml
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] {done:N0} aste studiate, {usable:N0} utilizzabili, {ModelUpdates:N0} aggiornamenti");
|
||||
}
|
||||
|
||||
if (done % 10 == 0 || done == pending.Count)
|
||||
progress?.Report(new ProgressStep(done, pending.Count, $"{usable:N0} utilizzabili, {ModelUpdates:N0} aggiornamenti"));
|
||||
|
||||
await Task.Yield(); // il motore delle aste ha la precedenza
|
||||
}
|
||||
|
||||
@@ -466,11 +576,19 @@ namespace AutoBidder.Ml
|
||||
|
||||
var remaining = pending.Count - done;
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] Studiate {done:N0} aste ({usable:N0} utilizzabili) in {sw.Elapsed.TotalSeconds:N0} s; " +
|
||||
(remaining > 0 ? $"ne restano {remaining:N0} per i prossimi avvii." : "archivio completo."));
|
||||
(remaining > 0 ? $"ne restano {remaining:N0}." : "archivio completo."));
|
||||
return (done, usable);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Save();
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] Studio interrotto dall'utente dopo {done:N0} aste ({usable:N0} utilizzabili)");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnLog?.Invoke($"[APPRENDIMENTO] Studio delle aste interrotto: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -523,14 +641,14 @@ namespace AutoBidder.Ml
|
||||
/// Il bandit impara dalle decisioni di quest'asta, ora che hanno un esito: per
|
||||
/// ogni istante di decisione si sa se una puntata lì sarebbe stata l'ultima.
|
||||
/// </summary>
|
||||
private static int LearnBanditFromAuction(string auctionId)
|
||||
private static int LearnBanditFromAuction(ThompsonBandit bandit, string auctionId)
|
||||
{
|
||||
var n = 0;
|
||||
foreach (var r in Db.Query("SELECT context_key, outcome FROM bot_decisions WHERE auction_id = ?1 AND context_key IS NOT NULL AND outcome IS NOT NULL", auctionId))
|
||||
{
|
||||
var outcome = r.Str("outcome");
|
||||
var unanswered = outcome is "won" or "would_win" or "missed";
|
||||
_bandit.Observe(r.Str("context_key"), unanswered);
|
||||
bandit.Observe(r.Str("context_key"), unanswered);
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
@@ -585,7 +703,7 @@ namespace AutoBidder.Ml
|
||||
// Le righe dell'asta sono in coda: prima devono essere sul disco.
|
||||
Db.Flush();
|
||||
var learned = IngestAuction(record.AuctionId);
|
||||
var decisioni = LearnBanditFromAuction(record.AuctionId);
|
||||
var decisioni = LearnBanditFromAuction(_bandit, record.AuctionId);
|
||||
|
||||
Save();
|
||||
|
||||
|
||||
@@ -78,6 +78,13 @@ namespace AutoBidder.Ml
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Dimentica la misura recente: il fattore torna a 1 finché non si rimisura.</summary>
|
||||
public void ResetCalibration()
|
||||
{
|
||||
_emaObserved = 0;
|
||||
_emaPredicted = 0;
|
||||
}
|
||||
|
||||
/// <summary>Probabilità riportata nella scala osservata di recente.</summary>
|
||||
public double PredictCalibrated(ReadOnlySpan<double> x) =>
|
||||
Math.Clamp(Predict(x) * CalibrationFactor, 0.0, 1.0);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Ottimo! Ora alcune cose:
|
||||
-
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using AutoBidder.Data;
|
||||
using AutoBidder.Ml;
|
||||
using AutoBidder.Models;
|
||||
using AutoBidder.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace AutoBidder.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Le operazioni massive riferiscono l'avanzamento, si annullano senza lasciare le
|
||||
/// cose a metà, e le manutenzioni dell'apprendimento fanno quello che dicono.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public class MaintenanceProgressTests
|
||||
{
|
||||
/// <summary>Raccoglie i passi sul posto: Progress<T> li rimanderebbe a un altro thread.</summary>
|
||||
private sealed class Raccolta : IProgress<ProgressStep>
|
||||
{
|
||||
public readonly List<ProgressStep> Passi = new();
|
||||
public void Report(ProgressStep value) { lock (Passi) Passi.Add(value); }
|
||||
}
|
||||
|
||||
private static CompletedAuctionRecord Asta(string key, double prezzo) => new()
|
||||
{
|
||||
AuctionId = "ric-" + Guid.NewGuid().ToString("N"),
|
||||
Name = key + " di prova",
|
||||
ProductKey = key,
|
||||
FinalPrice = prezzo,
|
||||
Winner = "qualcuno",
|
||||
Outcome = "Persa",
|
||||
BuyNowPrice = 11,
|
||||
EndedAt = DateTime.Now.AddMinutes(-1)
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Il_ricalcolo_dallo_storico_riferisce_lavanzamento_e_arriva_in_fondo()
|
||||
{
|
||||
var key = "ricalcolo-" + Guid.NewGuid().ToString("N")[..8];
|
||||
CompletedAuctionsStore.AppendMany(new[] { Asta(key, 1.10), Asta(key, 2.30), Asta(key, 0.70) });
|
||||
|
||||
var raccolta = new Raccolta();
|
||||
var n = ProductStatsStore.RebuildFromHistory(raccolta, CancellationToken.None);
|
||||
|
||||
Assert.True(n >= 3);
|
||||
Assert.NotEmpty(raccolta.Passi);
|
||||
var ultimo = raccolta.Passi[^1];
|
||||
Assert.Equal(n, ultimo.Total);
|
||||
Assert.Equal(n, ultimo.Done);
|
||||
|
||||
var scheda = ProductStatsStore.Find(key);
|
||||
Assert.NotNull(scheda);
|
||||
Assert.Equal(3, scheda!.AuctionsCompleted);
|
||||
Assert.Equal(0.70, scheda.MinFinalPrice);
|
||||
Assert.Equal(2.30, scheda.MaxFinalPrice);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_ricalcolo_annullato_lascia_le_schede_di_prima()
|
||||
{
|
||||
var key = "annulla-" + Guid.NewGuid().ToString("N")[..8];
|
||||
CompletedAuctionsStore.Append(Asta(key, 1.00));
|
||||
ProductStatsStore.RebuildFromHistory();
|
||||
var prima = ProductStatsStore.GetAll().Count;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
Assert.Throws<OperationCanceledException>(() => ProductStatsStore.RebuildFromHistory(null, cts.Token));
|
||||
Assert.Equal(prima, ProductStatsStore.GetAll().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Azzerare_la_calibrazione_riporta_il_fattore_a_uno()
|
||||
{
|
||||
var logit = new OnlineLogit(1) { CalibrationWindow = 100 };
|
||||
var x = new[] { 1.0 };
|
||||
|
||||
// Prima il modello impara che succede una volta su dieci, poi il mondo cambia e
|
||||
// succede nove volte su dieci: la calibrazione misura lo scarto e lo corregge.
|
||||
for (var i = 0; i < 2200; i++) logit.Update(x, i % 10 == 0);
|
||||
for (var i = 0; i < 300; i++) logit.Update(x, i % 10 != 0);
|
||||
|
||||
Assert.True(logit.CalibrationFactor > 1.05, $"fattore {logit.CalibrationFactor}");
|
||||
|
||||
logit.ResetCalibration();
|
||||
Assert.Equal(1.0, logit.CalibrationFactor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task Il_bandit_si_rifa_dalle_decisioni_con_esito()
|
||||
{
|
||||
var db = AuctionDatabase.Instance;
|
||||
var id = "bandit-" + 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) VALUES(?1, 'b', 'Bandit', ?2, ?2, ?2, 'closed', 'Persa')", id, now);
|
||||
foreach (var (esito, k) in new[] { ("would_win", 1), ("would_be_answered", 2), ("missed", 3) })
|
||||
db.ExecuteNow("INSERT INTO bot_decisions(decision_id, auction_id, ts, action, mode, context_key, outcome) VALUES(?1, ?2, ?3, 'BID', 'shadow', 'chiave-di-prova', ?4)",
|
||||
db.NextDecisionId(), id, now, esito);
|
||||
|
||||
var raccolta = new Raccolta();
|
||||
var (aste, osservazioni) = await LearningService.RebuildBanditFromDecisionsAsync(raccolta, CancellationToken.None);
|
||||
|
||||
Assert.True(aste >= 1);
|
||||
Assert.True(osservazioni >= 3);
|
||||
Assert.True(LearningService.Bandit.Observations >= 3);
|
||||
Assert.NotEmpty(raccolta.Passi);
|
||||
}
|
||||
}
|
||||
@@ -698,6 +698,47 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- La barra in alto di ogni scheda: stessa altezza ovunque, così passando da una
|
||||
scheda all'altra i pulsanti restano allo stesso posto. Il contenuto si centra
|
||||
in verticale da sé: le colonne sono StackPanel con VerticalAlignment=Center. -->
|
||||
<Style x:Key="TabToolbar" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Surface}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Setter Property="Padding" Value="10,0"/>
|
||||
<Setter Property="Height" Value="54"/>
|
||||
</Style>
|
||||
|
||||
<!-- Voce di un menu a comparsa (Popup): un pulsante piatto, allineato a sinistra. -->
|
||||
<Style x:Key="MenuEntry" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Font.UI}"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource Font.Size.Base}"/>
|
||||
<Setter Property="Padding" Value="10,7"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="7"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Brush.SurfaceHover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Pulsante icona a due stati: stesso aspetto di IconButton, e quando è acceso
|
||||
si riempie del colore d'accento. -->
|
||||
<Style x:Key="IconToggle" TargetType="ToggleButton">
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using AutoBidder.Models;
|
||||
@@ -156,6 +157,15 @@ namespace AutoBidder.Utilities
|
||||
lock (Sync)
|
||||
{
|
||||
var data = Load();
|
||||
RecordInto(data, summary, detail);
|
||||
Save(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Aggiorna la scheda dentro <paramref name="data"/>, qualunque elenco sia: quello vivo o uno in costruzione.</summary>
|
||||
private static void RecordInto(StoreData data, CompletedAuctionRecord summary, AuctionDetailRecord? detail)
|
||||
{
|
||||
{
|
||||
var card = FindInternal(data, summary.ProductKey);
|
||||
|
||||
if (card == null)
|
||||
@@ -207,7 +217,6 @@ namespace AutoBidder.Utilities
|
||||
}
|
||||
|
||||
SyncOptions(card);
|
||||
Save(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,27 +282,53 @@ namespace AutoBidder.Utilities
|
||||
/// concluse prima che questo archivio esistesse non lo hanno mai alimentato — e ogni
|
||||
/// volta che si sospetta che i totali non tornino.
|
||||
/// </summary>
|
||||
public static int RebuildFromHistory()
|
||||
/// <summary>Contrassegno di «generazione» delle schede nel database d'esercizio: cambia quando cambia il modo di contare.</summary>
|
||||
public const string GenerationDocument = "products-generation";
|
||||
public const string CurrentGeneration = "2";
|
||||
|
||||
/// <summary>
|
||||
/// Rilegge tutto lo storico e rifà da capo i numeri di ogni scheda: aste viste,
|
||||
/// prezzo minimo, massimo e medio, puntate del vincitore, vittorie, osservazioni.
|
||||
/// Riferisce l'avanzamento e si può annullare: si costruisce da parte e si
|
||||
/// sostituisce solo alla fine, così un annullamento a metà lascia le schede di
|
||||
/// prima invece di un elenco dimezzato.
|
||||
/// </summary>
|
||||
public static int RebuildFromHistory(IProgress<ProgressStep>? progress = null, CancellationToken ct = default)
|
||||
{
|
||||
var summaries = CompletedAuctionsStore.LoadAll();
|
||||
progress?.Report(new ProgressStep(0, summaries.Count, "leggo le schede dettagliate delle aste…"));
|
||||
|
||||
var details = AuctionDetailStore.LoadAll()
|
||||
.GroupBy(d => d.AuctionId)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
lock (Sync)
|
||||
{
|
||||
_cache = new StoreData();
|
||||
Save(_cache);
|
||||
}
|
||||
var fresh = new StoreData();
|
||||
var done = 0;
|
||||
|
||||
foreach (var summary in summaries.OrderBy(s => s.EndedAt))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
details.TryGetValue(summary.AuctionId, out var detail);
|
||||
RecordCompleted(summary, detail);
|
||||
if (!string.IsNullOrWhiteSpace(summary.ProductKey)) RecordInto(fresh, summary, detail);
|
||||
|
||||
done++;
|
||||
if (done % 25 == 0 || done == summaries.Count)
|
||||
progress?.Report(new ProgressStep(done, summaries.Count, $"{fresh.Products.Count:N0} prodotti"));
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
lock (Sync)
|
||||
{
|
||||
_cache = fresh;
|
||||
Save(fresh);
|
||||
}
|
||||
|
||||
SyncOptionsFromWatchList();
|
||||
|
||||
try { Data.OperationalDatabase.Instance.SetDocument(GenerationDocument, CurrentGeneration); } catch { }
|
||||
|
||||
return summaries.Count;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace AutoBidder.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Un passo di avanzamento di un'operazione lunga: quanti elementi sono fatti su
|
||||
/// quanti in tutto, e una riga di testo per dire a che punto si è. <c>Total</c> a
|
||||
/// zero = non si sa ancora quanto ci vuole (barra indeterminata).
|
||||
/// </summary>
|
||||
public readonly record struct ProgressStep(int Done, int Total, string? Message = null);
|
||||
|
||||
/// <summary>
|
||||
/// Com'è finita un'operazione seguita dalla finestra di avanzamento: con un
|
||||
/// risultato, annullata dall'utente, oppure con un errore. Chi la lancia legge qui
|
||||
/// invece di gestire eccezioni in un gestore di eventi.
|
||||
/// </summary>
|
||||
public sealed class ProgressOutcome<T>
|
||||
{
|
||||
public T? Result { get; private init; }
|
||||
public bool Cancelled { get; private init; }
|
||||
public Exception? Error { get; private init; }
|
||||
public bool Success => !Cancelled && Error == null;
|
||||
|
||||
public static ProgressOutcome<T> Ok(T result) => new() { Result = result };
|
||||
public static ProgressOutcome<T> CancelledByUser() => new() { Cancelled = true };
|
||||
public static ProgressOutcome<T> Failed(Exception error) => new() { Error = error };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user