Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3985935958 |
@@ -35,8 +35,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ═══════════════ BARRA STRUMENTI ═══════════════ -->
|
<!-- ═══════════════ BARRA STRUMENTI ═══════════════ -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
|||||||
@@ -148,8 +148,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ═══ Barra strumenti ═══ -->
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
|||||||
@@ -35,8 +35,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ═══ Barra strumenti ═══ -->
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
|||||||
@@ -33,8 +33,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ═══ Barra strumenti ═══ -->
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
@@ -87,6 +86,41 @@
|
|||||||
|
|
||||||
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="6,4"/>
|
<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}"
|
<Button x:Name="RetrainButton" Style="{StaticResource IconButton}"
|
||||||
Foreground="{DynamicResource Brush.Danger}"
|
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."
|
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)
|
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" +
|
"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);
|
"Ricomincia da capo", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
if (answer != MessageBoxResult.Yes) return;
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
@@ -364,12 +431,14 @@ namespace AutoBidder.Controls
|
|||||||
RetrainButton.IsEnabled = false;
|
RetrainButton.IsEnabled = false;
|
||||||
try
|
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
|
finally
|
||||||
{
|
{
|
||||||
RetrainButton.IsEnabled = true;
|
RetrainButton.IsEnabled = true;
|
||||||
Refresh();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,8 +121,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ═══ Barra strumenti ═══ -->
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
@@ -201,6 +200,11 @@
|
|||||||
|
|
||||||
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="6,4"/>
|
<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}"
|
<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."
|
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">
|
Click="ApplySuggestedButton_Click">
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ namespace AutoBidder.Controls
|
|||||||
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
|
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
|
||||||
=> RaiseEvent(new RoutedEventArgs(ClearAllClickedEvent, this));
|
=> 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)
|
private void UnwatchAllButton_Click(object sender, RoutedEventArgs e)
|
||||||
=> RaiseEvent(new RoutedEventArgs(UnwatchAllClickedEvent, this));
|
=> RaiseEvent(new RoutedEventArgs(UnwatchAllClickedEvent, this));
|
||||||
|
|
||||||
@@ -170,6 +173,9 @@ namespace AutoBidder.Controls
|
|||||||
public static readonly RoutedEvent ClearAllClickedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent ClearAllClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"ClearAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
"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(
|
public static readonly RoutedEvent UnwatchAllClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"UnwatchAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
"UnwatchAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
@@ -224,6 +230,12 @@ namespace AutoBidder.Controls
|
|||||||
remove { RemoveHandler(ClearAllClickedEvent, value); }
|
remove { RemoveHandler(ClearAllClickedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler RecalculateClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(RecalculateClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(RecalculateClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public event RoutedEventHandler UnwatchAllClicked
|
public event RoutedEventHandler UnwatchAllClicked
|
||||||
{
|
{
|
||||||
add { AddHandler(UnwatchAllClickedEvent, value); }
|
add { AddHandler(UnwatchAllClickedEvent, value); }
|
||||||
|
|||||||
@@ -217,6 +217,47 @@ namespace AutoBidder
|
|||||||
Log("[PRODOTTI] Elenco svuotato", LogLevel.Info);
|
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)
|
private void Products_ClearAllClicked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var answer = MessageBox.Show(this,
|
var answer = MessageBox.Show(this,
|
||||||
|
|||||||
@@ -273,20 +273,44 @@ namespace AutoBidder
|
|||||||
var backfill = new Services.WinnerBidsBackfill(transport);
|
var backfill = new Services.WinnerBidsBackfill(transport);
|
||||||
backfill.OnLog += m => Dispatcher.Invoke(() => Log(m, LogLevel.Warning));
|
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);
|
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();
|
LoadStatistics();
|
||||||
LoadProducts();
|
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"
|
ClearAllClicked="Products_ClearAllClicked"
|
||||||
UnwatchAllClicked="Products_UnwatchAllClicked"
|
UnwatchAllClicked="Products_UnwatchAllClicked"
|
||||||
RemoveUnwatchedClicked="Products_RemoveUnwatchedClicked"
|
RemoveUnwatchedClicked="Products_RemoveUnwatchedClicked"
|
||||||
|
RecalculateClicked="Products_RecalculateClicked"
|
||||||
ApplySuggestedClicked="Products_ApplySuggestedClicked"
|
ApplySuggestedClicked="Products_ApplySuggestedClicked"
|
||||||
ApplyAllSuggestedClicked="Products_ApplyAllSuggestedClicked"
|
ApplyAllSuggestedClicked="Products_ApplyAllSuggestedClicked"
|
||||||
ProductLimitsEdited="Products_ProductLimitsEdited"
|
ProductLimitsEdited="Products_ProductLimitsEdited"
|
||||||
@@ -222,8 +223,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Barra strumenti -->
|
<!-- Barra strumenti -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
|
||||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
|||||||
@@ -234,18 +234,14 @@ namespace AutoBidder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void EnsureProductCardsBuilt()
|
private void EnsureProductCardsBuilt()
|
||||||
{
|
{
|
||||||
const string Marker = "products-generation";
|
|
||||||
const string Generation = "2";
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var op = Data.OperationalDatabase.Instance;
|
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(() =>
|
_ = System.Threading.Tasks.Task.Run(() =>
|
||||||
{
|
{
|
||||||
var count = ProductStatsStore.RebuildFromHistory();
|
var count = ProductStatsStore.RebuildFromHistory();
|
||||||
op.SetDocument(Marker, Generation);
|
|
||||||
|
|
||||||
Dispatcher.BeginInvoke(() =>
|
Dispatcher.BeginInvoke(() =>
|
||||||
Log($"[PRODOTTI] Schede prodotto ricostruite dallo storico ({count} aste)", LogLevel.Info));
|
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,
|
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,
|
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
|
dell'apprendimento. Scheda **Impostazioni › Gestione del rischio**: paletti, HALT e
|
||||||
contabilità; **Impostazioni › Database** e **Manutenzione**: cartella dei database, «Elimina
|
contabilità; **Impostazioni › Database** e **Manutenzione**: cartella dei database, «Elimina
|
||||||
tutti i dati (tranne la login)», «Impostazioni di fabbrica». Scheda **Prodotti**: colonna
|
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.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AutoBidder.Data;
|
using AutoBidder.Data;
|
||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
@@ -220,16 +221,101 @@ namespace AutoBidder.Ml
|
|||||||
/// studiarle dal database. Serve quando si cambiano le variabili o si sospetta che
|
/// studiarle dal database. Serve quando si cambiano le variabili o si sospetta che
|
||||||
/// il modello abbia imparato da dati sbagliati.
|
/// il modello abbia imparato da dati sbagliati.
|
||||||
/// </summary>
|
/// </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();
|
Forget();
|
||||||
|
|
||||||
OnLog?.Invoke("[APPRENDIMENTO] Ricomincio da capo: profilo dallo storico, poi tutte le aste chiuse.");
|
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();
|
BootstrapProfileFromHistory();
|
||||||
|
|
||||||
// Un riaddestramento voluto ha diritto a tutto il tempo che serve.
|
// Un riaddestramento voluto ha diritto a tutto il tempo che serve.
|
||||||
var generose = new AppSettings { LearningBootstrapSecondsPerStart = Math.Max(600, settings.LearningBootstrapSecondsPerStart) };
|
return StudyPendingAsync(null, progress, ct);
|
||||||
return Task.Run(() => BootstrapAsync(generose));
|
}
|
||||||
|
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
@@ -413,17 +499,33 @@ namespace AutoBidder.Ml
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private static async Task BootstrapAsync(AppSettings settings)
|
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;
|
_bootstrapRunning = true;
|
||||||
|
|
||||||
|
var done = 0;
|
||||||
|
var usable = 0;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(3000).ConfigureAwait(false); // prima l'avvio, poi lo studio
|
|
||||||
|
|
||||||
List<string> pending;
|
List<string> pending;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -434,21 +536,26 @@ namespace AutoBidder.Ml
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnLog?.Invoke($"[APPRENDIMENTO] Elenco delle aste da studiare non disponibile: {ex.Message}");
|
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 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)
|
foreach (var id in pending)
|
||||||
{
|
{
|
||||||
if (sw.Elapsed > budget) break;
|
if (budget is { } limit && sw.Elapsed > limit) break;
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (IngestAuction(id)) usable++;
|
if (IngestAuction(id)) usable++;
|
||||||
done++;
|
done++;
|
||||||
@@ -459,6 +566,9 @@ namespace AutoBidder.Ml
|
|||||||
OnLog?.Invoke($"[APPRENDIMENTO] {done:N0} aste studiate, {usable:N0} utilizzabili, {ModelUpdates:N0} aggiornamenti");
|
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
|
await Task.Yield(); // il motore delle aste ha la precedenza
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,11 +576,19 @@ namespace AutoBidder.Ml
|
|||||||
|
|
||||||
var remaining = pending.Count - done;
|
var remaining = pending.Count - done;
|
||||||
OnLog?.Invoke($"[APPRENDIMENTO] Studiate {done:N0} aste ({usable:N0} utilizzabili) in {sw.Elapsed.TotalSeconds:N0} s; " +
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnLog?.Invoke($"[APPRENDIMENTO] Studio delle aste interrotto: {ex.Message}");
|
OnLog?.Invoke($"[APPRENDIMENTO] Studio delle aste interrotto: {ex.Message}");
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -523,14 +641,14 @@ namespace AutoBidder.Ml
|
|||||||
/// Il bandit impara dalle decisioni di quest'asta, ora che hanno un esito: per
|
/// 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.
|
/// ogni istante di decisione si sa se una puntata lì sarebbe stata l'ultima.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int LearnBanditFromAuction(string auctionId)
|
private static int LearnBanditFromAuction(ThompsonBandit bandit, string auctionId)
|
||||||
{
|
{
|
||||||
var n = 0;
|
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))
|
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 outcome = r.Str("outcome");
|
||||||
var unanswered = outcome is "won" or "would_win" or "missed";
|
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++;
|
n++;
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
@@ -585,7 +703,7 @@ namespace AutoBidder.Ml
|
|||||||
// Le righe dell'asta sono in coda: prima devono essere sul disco.
|
// Le righe dell'asta sono in coda: prima devono essere sul disco.
|
||||||
Db.Flush();
|
Db.Flush();
|
||||||
var learned = IngestAuction(record.AuctionId);
|
var learned = IngestAuction(record.AuctionId);
|
||||||
var decisioni = LearnBanditFromAuction(record.AuctionId);
|
var decisioni = LearnBanditFromAuction(_bandit, record.AuctionId);
|
||||||
|
|
||||||
Save();
|
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>
|
/// <summary>Probabilità riportata nella scala osservata di recente.</summary>
|
||||||
public double PredictCalibrated(ReadOnlySpan<double> x) =>
|
public double PredictCalibrated(ReadOnlySpan<double> x) =>
|
||||||
Math.Clamp(Predict(x) * CalibrationFactor, 0.0, 1.0);
|
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>
|
</Setter>
|
||||||
</Style>
|
</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
|
<!-- Pulsante icona a due stati: stesso aspetto di IconButton, e quando è acceso
|
||||||
si riempie del colore d'accento. -->
|
si riempie del colore d'accento. -->
|
||||||
<Style x:Key="IconToggle" TargetType="ToggleButton">
|
<Style x:Key="IconToggle" TargetType="ToggleButton">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
@@ -156,6 +157,15 @@ namespace AutoBidder.Utilities
|
|||||||
lock (Sync)
|
lock (Sync)
|
||||||
{
|
{
|
||||||
var data = Load();
|
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);
|
var card = FindInternal(data, summary.ProductKey);
|
||||||
|
|
||||||
if (card == null)
|
if (card == null)
|
||||||
@@ -207,7 +217,6 @@ namespace AutoBidder.Utilities
|
|||||||
}
|
}
|
||||||
|
|
||||||
SyncOptions(card);
|
SyncOptions(card);
|
||||||
Save(data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,27 +282,53 @@ namespace AutoBidder.Utilities
|
|||||||
/// concluse prima che questo archivio esistesse non lo hanno mai alimentato — e ogni
|
/// concluse prima che questo archivio esistesse non lo hanno mai alimentato — e ogni
|
||||||
/// volta che si sospetta che i totali non tornino.
|
/// volta che si sospetta che i totali non tornino.
|
||||||
/// </summary>
|
/// </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();
|
var summaries = CompletedAuctionsStore.LoadAll();
|
||||||
|
progress?.Report(new ProgressStep(0, summaries.Count, "leggo le schede dettagliate delle aste…"));
|
||||||
|
|
||||||
var details = AuctionDetailStore.LoadAll()
|
var details = AuctionDetailStore.LoadAll()
|
||||||
.GroupBy(d => d.AuctionId)
|
.GroupBy(d => d.AuctionId)
|
||||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
lock (Sync)
|
var fresh = new StoreData();
|
||||||
{
|
var done = 0;
|
||||||
_cache = new StoreData();
|
|
||||||
Save(_cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var summary in summaries.OrderBy(s => s.EndedAt))
|
foreach (var summary in summaries.OrderBy(s => s.EndedAt))
|
||||||
{
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
details.TryGetValue(summary.AuctionId, out var detail);
|
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();
|
SyncOptionsFromWatchList();
|
||||||
|
|
||||||
|
try { Data.OperationalDatabase.Instance.SetDocument(GenerationDocument, CurrentGeneration); } catch { }
|
||||||
|
|
||||||
return summaries.Count;
|
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