Documenti per chi installa da zero, pulizia per prodotto ed esborso, modalita' Gara
Tre richieste, piu' la causa vera della lentezza trovata misurando. PERCORSI. Le impostazioni e i dati vanno in Documenti\AutoBidder per chi installa da zero; sessione, riferimenti del sito e profilo del browser restano in %LocalAppData%, perche' la sessione e' protetta con DPAPI e legata all'utente Windows. Chi ha gia' un'installazione non viene spostato: se le impostazioni esistono in %LocalAppData% e non in Documenti si continua a usare quelle dove sono (scelta esplicita dell'utente, per non toccare mesi di raccolta). La radice si decide una volta sola all'avvio e non cambia piu' nel processo. La suite di test la dirotta in una cartella temporanea: senza, un test che tocca le impostazioni leggerebbe quelle vere. PULIZIA. Tre criteri nuovi: per prodotto (elenco con i conteggi, confronto senza maiuscole), solo osservate / con mie puntate (i due versi: storico compatto delle proprie partite, o storico "di mercato" senza le proprie mosse), prezzo finale oltre una percentuale del valore (le chiusure incaponite spostano medie e limiti consigliati verso l'alto per tutti; senza valore noto l'asta resta, non si indovina). Dopo ogni pulizia le statistiche per prodotto si ricostruiscono da zero dallo storico rimasto: sono un archivio a parte alimentato asta per asta, e avrebbero continuato a descrivere aste che non ci sono piu'. MODALITA'. Addestramento registra tutto; Gara toglie i poll grezzi nel dossier, le righe informative del registro dell'asta e il dettaglio esteso. Letta una volta all'avvio e immutabile fino alla chiusura, come chiesto: la scheda mostra la modalita' in vigore e avvisa del riavvio solo se la scelta differisce. Misurato prima di promettere: il lavoro che Gara evita costa 4,9 microsecondi per poll. A 57 aste per 17 poll al secondo sono 4,7 ms di CPU al secondo, mezzo punto di un core. La scrittura su disco era gia' asincrona. Gara fa bene al disco e ai dossier, non alla velocita'. LA LENTEZZA VERA. Ogni poll arrivava al thread dell'interfaccia: un BeginInvoke, una scansione lineare dei ViewModel e venti notifiche di proprieta'. Con trenta aste diecimila notifiche al secondo, con cinquantasette quasi ventimila. Adesso si manda all'interfaccia solo il poll in cui cambia qualcosa di visibile (prezzo, puntatore, stato, secondo intero, se e' mio), e comunque uno al secondo per asta perche' ping e contatori si rinfreschino. Il conto alla rovescia scorre gia' da solo con il battito da 200 ms. Il motore non e' toccato: la decisione di puntare vede ogni poll come prima. Misurato su 40 dossier veri, 146.138 poll: ne passano 16.375, l'88,8% viene scartato. Nove volte meno lavoro sul thread dell'interfaccia. Nella scheda Impostazioni entrano anche la fascia oraria e la pulizia dell'elenco, che il motore usava gia' ma che non si potevano ancora cambiare. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,11 @@ namespace AutoBidder
|
|||||||
AppPaths.Configure(settings.DataFolder, settings.StatsFolder, settings.LogFolder);
|
AppPaths.Configure(settings.DataFolder, settings.StatsFolder, settings.LogFolder);
|
||||||
AppPaths.EnsureFolders();
|
AppPaths.EnsureFolders();
|
||||||
|
|
||||||
|
// La modalità della sessione si fissa qui e non cambia più fino alla chiusura:
|
||||||
|
// chi salva le impostazioni a metà sessione vede l'avviso di riavvio, non un
|
||||||
|
// motore che cambia comportamento sotto i piedi.
|
||||||
|
RuntimeProfile.Initialize(settings.SessionMode);
|
||||||
|
|
||||||
// Le versioni precedenti scrivevano parte dei dati in Roaming e parte in Local,
|
// Le versioni precedenti scrivevano parte dei dati in Roaming e parte in Local,
|
||||||
// senza sottocartelle: senza questo passaggio storico e aste sparirebbero.
|
// senza sottocartelle: senza questo passaggio storico e aste sparirebbero.
|
||||||
AppPaths.MigrateLegacyFiles();
|
AppPaths.MigrateLegacyFiles();
|
||||||
@@ -34,6 +39,8 @@ namespace AutoBidder
|
|||||||
// proprio ciò che non si riesce a leggere a video.
|
// proprio ciò che non si riesce a leggere a video.
|
||||||
TextLogService.SessionStarted(AppInfo.Version);
|
TextLogService.SessionStarted(AppInfo.Version);
|
||||||
TextLogService.PurgeOldLogs();
|
TextLogService.PurgeOldLogs();
|
||||||
|
TextLogService.App("INFO", $"[MODALITÀ] {RuntimeProfile.Describe()}");
|
||||||
|
TextLogService.App("INFO", $"[PERCORSI] configurazione in {AppPaths.ConfigRoot}, dati in {AppPaths.DataFolder}");
|
||||||
|
|
||||||
// Le righe di registro delle aste confluiscono nei rispettivi dossier.
|
// Le righe di registro delle aste confluiscono nei rispettivi dossier.
|
||||||
AuctionDossier.CaptureAuctionLogs();
|
AuctionDossier.CaptureAuctionLogs();
|
||||||
|
|||||||
@@ -44,6 +44,57 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE 0-ter: Modalità della sessione -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Modalità della sessione"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Addestramento registra tutto: ogni interrogazione nel dossier, ogni decisione della strategia, il dettaglio esteso delle aste concluse. È la modalità con cui si raccolgono i dati per tarare le regole. Gara fa solo il lavoro che serve a puntare: niente poll grezzi nel dossier, niente righe informative nel registro dell'asta, niente dettaglio esteso. Restano puntate, esiti, errori e lo storico delle aste concluse. Cosa aspettarsi, misurato: il lavoro che Gara evita costa 4,9 microsecondi per interrogazione — con 57 aste a 17 poll al secondo è mezzo punto percentuale di un core. Il guadagno vero è sul disco e sulla leggibilità dei dossier, non sulla velocità. La modalità vale per tutta la sessione: cambiarla richiede il riavvio."/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<RadioButton x:Name="ModeTrainingRadio" Content="Addestramento"
|
||||||
|
GroupName="SessionMode" IsChecked="True"
|
||||||
|
Checked="SessionModeRadio_Checked"
|
||||||
|
ToolTip="Registrazione completa: dossier con ogni poll, registro della strategia, dettaglio esteso."/>
|
||||||
|
<RadioButton x:Name="ModeRaceRadio" Content="Gara" Margin="16,0,0,0"
|
||||||
|
GroupName="SessionMode"
|
||||||
|
Checked="SessionModeRadio_Checked"
|
||||||
|
ToolTip="Solo ciò che serve a puntare. Restano puntate, esiti, errori e lo storico delle aste concluse."/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock x:Name="ModeCurrentText" Margin="0,8,0,0"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}" FontSize="11.5"/>
|
||||||
|
<TextBlock x:Name="ModeRestartNotice" Margin="0,6,0,0" Visibility="Collapsed"
|
||||||
|
Foreground="{DynamicResource Brush.Warning}" TextWrapping="Wrap"
|
||||||
|
Text="La modalità scelta si applica al prossimo avvio: salva e riavvia l'applicazione."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE 0-quater: Fascia oraria e pulizia dell'elenco -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Fascia oraria sospesa e pulizia dell'elenco"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Fascia oraria: su 6060 aste concluse la stessa asta chiude al 4,6% del valore alle 12 e al 5,3% fra le 10 e le 13, ma all'8,6% a mezzanotte e al 9,7% alle 9 — quasi il doppio, e con 26-29 puntate per vincere invece di 21-24. Nella fascia le puntate si sospendono ma l'asta resta Attiva e riprende da sola. Inizio incluso, fine esclusa; una fascia a cavallo di mezzanotte (22-6) funziona; inizio uguale a fine spegne il filtro. Pulizia dell'elenco: le aste concluse spariscono da sole dal monitor, tranne quelle che vale la pena riguardare."/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||||
|
<CheckBox x:Name="QuietHoursCheckBox" Content="Sospendi le puntate dalle" VerticalAlignment="Center"/>
|
||||||
|
<TextBox x:Name="QuietHoursStartTextBox" Text="0" Width="40" Margin="8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="alle" VerticalAlignment="Center" Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
<TextBox x:Name="QuietHoursEndTextBox" Text="10" Width="40" Margin="8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="(ore, 0-23)" VerticalAlignment="Center" Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<CheckBox x:Name="AutoRemoveFinishedCheckBox" Content="Togli da sole le aste concluse dall'elenco" Margin="0,4"/>
|
||||||
|
<CheckBox x:Name="AutoRemoveKeepMyBidsCheckBox" Content="…ma trattieni quelle su cui ho puntato" Margin="20,2,0,0"
|
||||||
|
ToolTip="Sono le uniche con il costo reale dentro: restano per controllarle a mano."/>
|
||||||
|
<CheckBox x:Name="AutoRemoveKeepWonCheckBox" Content="…ma trattieni quelle vinte" Margin="20,2,0,0"
|
||||||
|
ToolTip="C'è da confermare l'acquisto su Bidoo: una vittoria sparita dall'elenco è il modo più semplice di dimenticarsene."/>
|
||||||
|
<CheckBox x:Name="AutoRemoveKeepUnclearCheckBox" Content="…ma trattieni quelle di cui non si è vista la fine" Margin="20,2,0,0"
|
||||||
|
ToolTip="Esito da controllare: sono esattamente quelle da guardare."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- SEZIONE 0-bis: Sessione / Autenticazione -->
|
<!-- SEZIONE 0-bis: Sessione / Autenticazione -->
|
||||||
<Border Style="{StaticResource CardBlock}">
|
<Border Style="{StaticResource CardBlock}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|||||||
@@ -48,6 +48,30 @@ namespace AutoBidder.Controls
|
|||||||
Utilities.ThemeManager.SetAndSave(false);
|
Utilities.ThemeManager.SetAndSave(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Modalità della sessione ──────────────────────────────────────
|
||||||
|
|
||||||
|
private void SessionModeRadio_Checked(object sender, RoutedEventArgs e) =>
|
||||||
|
RefreshModeNotice(Utilities.RuntimeProfile.Mode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mostra che modalità è in vigore adesso, e l'avviso di riavvio solo se la scelta
|
||||||
|
/// nella scheda differisce da quella. Un avviso sempre acceso non lo legge nessuno.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshModeNotice(string modalitaInVigore)
|
||||||
|
{
|
||||||
|
if (ModeRestartNotice == null || ModeCurrentText == null) return;
|
||||||
|
|
||||||
|
var scelta = ModeRaceRadio?.IsChecked == true
|
||||||
|
? Utilities.RuntimeProfile.Gara
|
||||||
|
: Utilities.RuntimeProfile.Addestramento;
|
||||||
|
|
||||||
|
ModeCurrentText.Text = $"In vigore adesso: {modalitaInVigore}";
|
||||||
|
ModeRestartNotice.Visibility =
|
||||||
|
string.Equals(scelta, modalitaInVigore, System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
? Visibility.Collapsed
|
||||||
|
: Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
// Non servono propriet� wrapper - MainWindow.xaml.cs accede direttamente ai controlli tramite:
|
// Non servono propriet� wrapper - MainWindow.xaml.cs accede direttamente ai controlli tramite:
|
||||||
// Settings.DefaultBidBeforeDeadlineMsTextBox (definito nel XAML con x:Name)
|
// Settings.DefaultBidBeforeDeadlineMsTextBox (definito nel XAML con x:Name)
|
||||||
// Settings.MaxLogLinesPerAuctionTextBox (definito nel XAML con x:Name)
|
// Settings.MaxLogLinesPerAuctionTextBox (definito nel XAML con x:Name)
|
||||||
|
|||||||
@@ -107,6 +107,23 @@ namespace AutoBidder
|
|||||||
Settings.DetailedStatsCheckBox.IsChecked = settings.DetailedStatsEnabled;
|
Settings.DetailedStatsCheckBox.IsChecked = settings.DetailedStatsEnabled;
|
||||||
Settings.CatalogCacheSecondsTextBox.Text = settings.CatalogCacheSeconds.ToString();
|
Settings.CatalogCacheSecondsTextBox.Text = settings.CatalogCacheSeconds.ToString();
|
||||||
|
|
||||||
|
// Modalità: si mostra quella salvata; l'avviso compare se differisce da
|
||||||
|
// quella in vigore, che è stata fissata all'avvio e non cambia fino al riavvio.
|
||||||
|
if (string.Equals(settings.SessionMode, RuntimeProfile.Gara, StringComparison.OrdinalIgnoreCase))
|
||||||
|
Settings.ModeRaceRadio.IsChecked = true;
|
||||||
|
else
|
||||||
|
Settings.ModeTrainingRadio.IsChecked = true;
|
||||||
|
Settings.RefreshModeNotice(RuntimeProfile.Mode);
|
||||||
|
|
||||||
|
Settings.QuietHoursCheckBox.IsChecked = settings.QuietHoursEnabled;
|
||||||
|
Settings.QuietHoursStartTextBox.Text = settings.QuietHoursStart.ToString();
|
||||||
|
Settings.QuietHoursEndTextBox.Text = settings.QuietHoursEnd.ToString();
|
||||||
|
|
||||||
|
Settings.AutoRemoveFinishedCheckBox.IsChecked = settings.AutoRemoveFinished;
|
||||||
|
Settings.AutoRemoveKeepMyBidsCheckBox.IsChecked = settings.AutoRemoveKeepWithMyBids;
|
||||||
|
Settings.AutoRemoveKeepWonCheckBox.IsChecked = settings.AutoRemoveKeepWon;
|
||||||
|
Settings.AutoRemoveKeepUnclearCheckBox.IsChecked = settings.AutoRemoveKeepUnclear;
|
||||||
|
|
||||||
Settings.WriteAppLogCheckBox.IsChecked = settings.WriteAppLogFile;
|
Settings.WriteAppLogCheckBox.IsChecked = settings.WriteAppLogFile;
|
||||||
Settings.WriteFreeBidsLogCheckBox.IsChecked = settings.WriteFreeBidsLogFile;
|
Settings.WriteFreeBidsLogCheckBox.IsChecked = settings.WriteFreeBidsLogFile;
|
||||||
Settings.WriteDossiersCheckBox.IsChecked = settings.WriteAuctionDossiers;
|
Settings.WriteDossiersCheckBox.IsChecked = settings.WriteAuctionDossiers;
|
||||||
@@ -413,6 +430,22 @@ namespace AutoBidder
|
|||||||
|
|
||||||
settings.DetailedStatsEnabled = Settings.DetailedStatsCheckBox.IsChecked ?? true;
|
settings.DetailedStatsEnabled = Settings.DetailedStatsCheckBox.IsChecked ?? true;
|
||||||
|
|
||||||
|
settings.SessionMode = Settings.ModeRaceRadio.IsChecked == true
|
||||||
|
? RuntimeProfile.Gara
|
||||||
|
: RuntimeProfile.Addestramento;
|
||||||
|
Settings.RefreshModeNotice(RuntimeProfile.Mode);
|
||||||
|
|
||||||
|
settings.QuietHoursEnabled = Settings.QuietHoursCheckBox.IsChecked ?? true;
|
||||||
|
if (int.TryParse(Settings.QuietHoursStartTextBox.Text?.Trim(), out var qStart) && qStart is >= 0 and <= 23)
|
||||||
|
settings.QuietHoursStart = qStart;
|
||||||
|
if (int.TryParse(Settings.QuietHoursEndTextBox.Text?.Trim(), out var qEnd) && qEnd is >= 0 and <= 24)
|
||||||
|
settings.QuietHoursEnd = qEnd;
|
||||||
|
|
||||||
|
settings.AutoRemoveFinished = Settings.AutoRemoveFinishedCheckBox.IsChecked ?? true;
|
||||||
|
settings.AutoRemoveKeepWithMyBids = Settings.AutoRemoveKeepMyBidsCheckBox.IsChecked ?? true;
|
||||||
|
settings.AutoRemoveKeepWon = Settings.AutoRemoveKeepWonCheckBox.IsChecked ?? true;
|
||||||
|
settings.AutoRemoveKeepUnclear = Settings.AutoRemoveKeepUnclearCheckBox.IsChecked ?? true;
|
||||||
|
|
||||||
settings.WriteAppLogFile = Settings.WriteAppLogCheckBox.IsChecked ?? true;
|
settings.WriteAppLogFile = Settings.WriteAppLogCheckBox.IsChecked ?? true;
|
||||||
settings.WriteFreeBidsLogFile = Settings.WriteFreeBidsLogCheckBox.IsChecked ?? true;
|
settings.WriteFreeBidsLogFile = Settings.WriteFreeBidsLogCheckBox.IsChecked ?? true;
|
||||||
settings.WriteAuctionDossiers = Settings.WriteDossiersCheckBox.IsChecked ?? true;
|
settings.WriteAuctionDossiers = Settings.WriteDossiersCheckBox.IsChecked ?? true;
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ namespace AutoBidder
|
|||||||
|
|
||||||
var detail = BuildDetail(auction, state, record);
|
var detail = BuildDetail(auction, state, record);
|
||||||
|
|
||||||
if (settings.DetailedStatsEnabled)
|
// In Gara il dettaglio esteso non si scrive: vedi RuntimeProfile.
|
||||||
|
if (settings.DetailedStatsEnabled && RuntimeProfile.DetailedStats)
|
||||||
{
|
{
|
||||||
AuctionDetailStore.Append(detail);
|
AuctionDetailStore.Append(detail);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,11 +105,26 @@ namespace AutoBidder
|
|||||||
|
|
||||||
if (dialog.ShowDialog() != true) return;
|
if (dialog.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
// Le statistiche per prodotto sono un archivio a parte, alimentato asta per
|
||||||
|
// asta man mano che chiudono: dopo una pulizia descriverebbero aste che non ci
|
||||||
|
// sono più — e sono loro a decidere i limiti consigliati e i tetti di puntate.
|
||||||
|
// Si ricostruiscono da zero dallo storico appena pulito, l'unica fonte rimasta.
|
||||||
|
var ricostruite = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ricostruite = ProductStatsStore.RebuildFromHistory();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[STATISTICHE] Ricalcolo per prodotto non riuscito: {ex.Message}", LogLevel.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
LoadStatistics();
|
LoadStatistics();
|
||||||
LoadProducts();
|
LoadProducts();
|
||||||
|
|
||||||
Log($"[STORICO] Pulizia: tolte {dialog.RemovedCount} aste. " +
|
Log($"[STORICO] Pulizia: tolte {dialog.RemovedCount} aste; statistiche per prodotto " +
|
||||||
$"Copia di sicurezza in {dialog.BackupPath}", LogLevel.Success);
|
$"ricalcolate su {ricostruite} aste rimaste. Copia di sicurezza in {dialog.BackupPath}",
|
||||||
|
LogLevel.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -46,6 +46,49 @@
|
|||||||
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
||||||
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkObservedOnly" Margin="0,4"
|
||||||
|
Content="Aste solo osservate (senza mie puntate)"
|
||||||
|
ToolTip="Tiene le aste su cui hai puntato — sono le uniche con il costo reale dentro — e toglie tutte le altre. Utile per uno storico compatto delle tue partite."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkWithMyBids" Margin="0,4"
|
||||||
|
Content="Aste su cui ho puntato"
|
||||||
|
ToolTip="Il verso opposto: tiene solo il mercato. Un'asta in cui sei intervenuto ha un prezzo finale che hai contribuito a fare, e per stimare cosa succede senza di te va tolta."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<CheckBox x:Name="ChkPriceRatio" VerticalAlignment="Center"
|
||||||
|
Content="Aste chiuse oltre il"
|
||||||
|
ToolTip="Una chiusura al 60% del valore su un buono da 100 € non descrive il mercato: descrive due persone che si sono incaponite. Tenerla sposta le medie e i limiti consigliati verso l'alto per tutti. Sul tuo storico la chiusura mediana sta fra il 5% e il 6% del valore, e tre aste su quattro chiudono sotto il 15%."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
<TextBox x:Name="PriceRatioBox" Text="40" Width="50" Margin="8,0"
|
||||||
|
VerticalAlignment="Center" TextChanged="Option_Changed"/>
|
||||||
|
<TextBlock Text="% del valore del prodotto" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Per prodotto: i prodotti presenti nello storico, col numero di aste -->
|
||||||
|
<Expander x:Name="ProductsExpander" Margin="0,10,0,0" IsExpanded="False"
|
||||||
|
Foreground="{DynamicResource Brush.Text}">
|
||||||
|
<Expander.Header>
|
||||||
|
<TextBlock x:Name="ProductsHeader" Text="Per prodotto: nessuno scelto"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</Expander.Header>
|
||||||
|
<ListBox x:Name="ProductsList" MaxHeight="170" Margin="0,6,0,0"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<CheckBox Content="{Binding Label}"
|
||||||
|
IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Expander>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
|
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
|
||||||
Content="Aste seguite per meno del"
|
Content="Aste seguite per meno del"
|
||||||
|
|||||||
@@ -20,10 +20,36 @@ namespace AutoBidder.Dialogs
|
|||||||
{
|
{
|
||||||
private readonly List<CompletedAuctionRecord> _records;
|
private readonly List<CompletedAuctionRecord> _records;
|
||||||
|
|
||||||
|
/// <summary>Una riga dell'elenco prodotti: chiave, etichetta con il conteggio, spunta.</summary>
|
||||||
|
private sealed class ProductRow
|
||||||
|
{
|
||||||
|
public string Key { get; init; } = "";
|
||||||
|
public string Label { get; init; } = "";
|
||||||
|
public bool Selected { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<ProductRow> _products;
|
||||||
|
|
||||||
public StatsCleanupDialog(List<CompletedAuctionRecord> records)
|
public StatsCleanupDialog(List<CompletedAuctionRecord> records)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_records = records ?? new List<CompletedAuctionRecord>();
|
_records = records ?? new List<CompletedAuctionRecord>();
|
||||||
|
|
||||||
|
// I prodotti presenti nello storico, dal più numeroso: è l'ordine in cui
|
||||||
|
// uno li cerca quando vuole fare spazio.
|
||||||
|
_products = _records
|
||||||
|
.Where(r => !string.IsNullOrWhiteSpace(r.ProductKey))
|
||||||
|
.GroupBy(r => r.ProductKey, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(g => new ProductRow
|
||||||
|
{
|
||||||
|
Key = g.Key,
|
||||||
|
Label = $"{g.Select(r => r.Name).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) ?? g.Key} ({g.Count()})"
|
||||||
|
})
|
||||||
|
.OrderByDescending(p => p.Label.Length > 0 ? _records.Count(r => string.Equals(r.ProductKey, p.Key, StringComparison.OrdinalIgnoreCase)) : 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
ProductsList.ItemsSource = _products;
|
||||||
|
|
||||||
Loaded += (_, _) => Refresh();
|
Loaded += (_, _) => Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +88,22 @@ namespace AutoBidder.Dialogs
|
|||||||
options.OlderThan = DateTime.Now.AddDays(-days);
|
options.OlderThan = DateTime.Now.AddDays(-days);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.RemoveObservedOnly = ChkObservedOnly.IsChecked == true;
|
||||||
|
options.RemoveWithMyBids = ChkWithMyBids.IsChecked == true;
|
||||||
|
|
||||||
|
if (ChkPriceRatio.IsChecked == true &&
|
||||||
|
double.TryParse(PriceRatioBox.Text.Trim().Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var ratio) &&
|
||||||
|
ratio > 0)
|
||||||
|
{
|
||||||
|
options.MaxPriceRatioPercent = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scelti = _products.Where(p => p.Selected).Select(p => p.Key).ToList();
|
||||||
|
if (scelti.Count > 0)
|
||||||
|
options.RemoveProductKeys = new HashSet<string>(scelti, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +113,14 @@ namespace AutoBidder.Dialogs
|
|||||||
{
|
{
|
||||||
if (SummaryText == null) return;
|
if (SummaryText == null) return;
|
||||||
|
|
||||||
|
if (ProductsHeader != null)
|
||||||
|
{
|
||||||
|
var n = _products.Count(p => p.Selected);
|
||||||
|
ProductsHeader.Text = n == 0
|
||||||
|
? $"Per prodotto: nessuno scelto ({_products.Count} nello storico)"
|
||||||
|
: $"Per prodotto: {n} scelti";
|
||||||
|
}
|
||||||
|
|
||||||
var report = StatsMaintenance.Preview(_records, ReadOptions());
|
var report = StatsMaintenance.Preview(_records, ReadOptions());
|
||||||
|
|
||||||
SummaryText.Text = report.Removed == 0
|
SummaryText.Text = report.Removed == 0
|
||||||
|
|||||||
@@ -273,14 +273,46 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// ===== AUCTION MONITOR EVENT HANDLERS =====
|
// ===== AUCTION MONITOR EVENT HANDLERS =====
|
||||||
|
|
||||||
|
// ── Coalescenza degli aggiornamenti verso l'interfaccia ──────────
|
||||||
|
//
|
||||||
|
// Con il tetto delle richieste tolto ogni asta produce ~17 interrogazioni al
|
||||||
|
// secondo, e ognuna arrivava qui: un BeginInvoke, una scansione lineare dei
|
||||||
|
// ViewModel, e venti notifiche di proprietà. Con trenta aste sono diecimila
|
||||||
|
// notifiche al secondo sul thread dell'interfaccia, con cinquantasette quasi
|
||||||
|
// ventimila. È questo che rende l'applicazione lenta — non il disco, la cui
|
||||||
|
// scrittura era già asincrona e costa mezzo punto di CPU.
|
||||||
|
//
|
||||||
|
// La stragrande maggioranza di quei poll non porta niente di nuovo da mostrare:
|
||||||
|
// stesso prezzo, stesso puntatore, stesso stato, stesso secondo sul cronometro.
|
||||||
|
// Il conto alla rovescia scorre da solo grazie al battito da 200 ms, che legge la
|
||||||
|
// scadenza ancorata e non ha bisogno del poll. Quindi si manda all'interfaccia
|
||||||
|
// solo il poll in cui cambia qualcosa di visibile, e comunque uno al secondo per
|
||||||
|
// asta perché ping e contatori si rinfreschino.
|
||||||
|
//
|
||||||
|
// Il motore non è toccato: la decisione di puntare vede ogni poll come prima.
|
||||||
|
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, (string Firma, long Tick)> _ultimoInviato = new();
|
||||||
|
|
||||||
|
private static string FirmaVisibile(AuctionState s) =>
|
||||||
|
string.Concat(s.Price.ToString("F2"), '|', s.LastBidder, '|', s.Status, '|', (int)s.Timer, '|', s.IsMyBid ? '1' : '0');
|
||||||
|
|
||||||
private void AuctionMonitor_OnAuctionUpdated(AuctionState state)
|
private void AuctionMonitor_OnAuctionUpdated(AuctionState state)
|
||||||
{
|
{
|
||||||
|
var firma = FirmaVisibile(state);
|
||||||
|
var adesso = Environment.TickCount64;
|
||||||
|
|
||||||
|
if (_ultimoInviato.TryGetValue(state.AuctionId, out var prec) &&
|
||||||
|
prec.Firma == firma && adesso - prec.Tick < 1000)
|
||||||
|
{
|
||||||
|
return; // niente di nuovo da mostrare, e l'ultimo rinfresco è recente
|
||||||
|
}
|
||||||
|
|
||||||
|
_ultimoInviato[state.AuctionId] = (firma, adesso);
|
||||||
|
|
||||||
Dispatcher.BeginInvoke(() =>
|
Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
var vm = _auctionViewModels.FirstOrDefault(a => a.AuctionId == state.AuctionId);
|
var vm = _auctionViewModels.FirstOrDefault(a => a.AuctionId == state.AuctionId);
|
||||||
if (vm == null) return;
|
if (vm == null) return;
|
||||||
|
|
||||||
// Sempre: aggiorna la riga in griglia (poche proprietà, costo trascurabile).
|
|
||||||
vm.UpdateState(state);
|
vm.UpdateState(state);
|
||||||
|
|
||||||
// I pannelli di dettaglio mostrano SOLO l'asta selezionata: ricostruirli
|
// I pannelli di dettaglio mostrano SOLO l'asta selezionata: ricostruirli
|
||||||
|
|||||||
+18
-1
File diff suppressed because one or more lines are too long
@@ -452,7 +452,7 @@ namespace AutoBidder.Services
|
|||||||
// Il dossier registra tutto ciò che passa di qui: è materiale che esiste
|
// Il dossier registra tutto ciò che passa di qui: è materiale che esiste
|
||||||
// solo adesso.
|
// solo adesso.
|
||||||
var pollSettings = Settings;
|
var pollSettings = Settings;
|
||||||
AuctionDossier.OpenFor(auction, pollSettings)?.Poll(state, pollSettings.DossierIncludeRawPolls);
|
AuctionDossier.OpenFor(auction, pollSettings)?.Poll(state, pollSettings.DossierIncludeRawPolls && RuntimeProfile.RawPolls);
|
||||||
|
|
||||||
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
|
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -870,7 +870,7 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auction.AddLog($"✓ Convenienza OK (check={settings.ValueCheckEnabled}, buyNow={auction.BuyNowPrice?.ToString("F2") ?? "N/D"})",
|
if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Convenienza OK (check={settings.ValueCheckEnabled}, buyNow={auction.BuyNowPrice?.ToString("F2") ?? "N/D"})",
|
||||||
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Value);
|
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,7 +925,7 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
else if (session != null)
|
else if (session != null)
|
||||||
{
|
{
|
||||||
auction.AddLog($"✓ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})",
|
if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})",
|
||||||
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit);
|
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -933,7 +933,7 @@ namespace AutoBidder.Services
|
|||||||
// CONTROLLO 2: Non puntare se sono già il vincitore corrente
|
// CONTROLLO 2: Non puntare se sono già il vincitore corrente
|
||||||
if (state.IsMyBid)
|
if (state.IsMyBid)
|
||||||
{
|
{
|
||||||
auction.AddLog($"✓ Sono già vincitore corrente - skip",
|
if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Sono già vincitore corrente - skip",
|
||||||
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.BidAttempt);
|
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.BidAttempt);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -954,7 +954,7 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
if (maxBids > 0)
|
if (maxBids > 0)
|
||||||
{
|
{
|
||||||
auction.AddLog($"✓ Puntate OK ({usedBids}/{maxBids})",
|
if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Puntate OK ({usedBids}/{maxBids})",
|
||||||
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit);
|
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -976,7 +976,7 @@ namespace AutoBidder.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auction.AddLog($"✓ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "∞")}])",
|
if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "∞")}])",
|
||||||
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Price);
|
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Price);
|
||||||
|
|
||||||
|
|
||||||
@@ -1020,7 +1020,7 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auction.AddLog($"✓ Tutti i controlli ShouldBid superati",
|
if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Tutti i controlli ShouldBid superati",
|
||||||
Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt);
|
Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -378,4 +378,87 @@ public class StatsMaintenanceTests
|
|||||||
|
|
||||||
Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys);
|
Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Per prodotto, per esborso, per anomalia ───────────────────────────
|
||||||
|
|
||||||
|
private static CompletedAuctionRecord Con(
|
||||||
|
string id, string key, int mie = 0, double price = 1.00, double? value = 11.00)
|
||||||
|
{
|
||||||
|
var r = R(id: id, key: key, price: price, value: value, bidsFree: 1);
|
||||||
|
r.MyBidsUsed = mie;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Toglie_solo_i_prodotti_scelti_senza_badare_alle_maiuscole()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord>
|
||||||
|
{
|
||||||
|
Con("1", "buono a"), Con("2", "buono b"), Con("3", "buono a")
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new StatsMaintenance.CleanupOptions
|
||||||
|
{
|
||||||
|
RemoveProductKeys = new HashSet<string>(new[] { "BUONO A" }, StringComparer.OrdinalIgnoreCase)
|
||||||
|
};
|
||||||
|
|
||||||
|
var kept = StatsMaintenance.Apply(list, options);
|
||||||
|
|
||||||
|
Assert.Single(kept);
|
||||||
|
Assert.Equal("2", kept[0].AuctionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Solo_osservate_tiene_quelle_con_mie_puntate()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord> { Con("mia", "x", mie: 5), Con("vista", "x") };
|
||||||
|
|
||||||
|
var kept = StatsMaintenance.Apply(list,
|
||||||
|
new StatsMaintenance.CleanupOptions { RemoveObservedOnly = true });
|
||||||
|
|
||||||
|
Assert.Single(kept);
|
||||||
|
Assert.Equal("mia", kept[0].AuctionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Con_mie_puntate_tiene_solo_il_mercato()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord> { Con("mia", "x", mie: 5), Con("vista", "x") };
|
||||||
|
|
||||||
|
var kept = StatsMaintenance.Apply(list,
|
||||||
|
new StatsMaintenance.CleanupOptions { RemoveWithMyBids = true });
|
||||||
|
|
||||||
|
Assert.Single(kept);
|
||||||
|
Assert.Equal("vista", kept[0].AuctionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Il_prezzo_fuori_scala_si_misura_sul_valore_e_senza_valore_non_si_indovina()
|
||||||
|
{
|
||||||
|
// 6 EUR su 10 di valore = 60%; 0,50 su 10 = 5%.
|
||||||
|
var list = new List<CompletedAuctionRecord>
|
||||||
|
{
|
||||||
|
Con("cara", "x", price: 6.00, value: 10.00),
|
||||||
|
Con("normale", "x", price: 0.50, value: 10.00),
|
||||||
|
Con("senza valore", "x", price: 6.00, value: null),
|
||||||
|
};
|
||||||
|
|
||||||
|
var kept = StatsMaintenance.Apply(list,
|
||||||
|
new StatsMaintenance.CleanupOptions { MaxPriceRatioPercent = 40 });
|
||||||
|
|
||||||
|
Assert.Equal(2, kept.Count);
|
||||||
|
Assert.DoesNotContain(kept, r => r.AuctionId == "cara");
|
||||||
|
Assert.Contains(kept, r => r.AuctionId == "senza valore");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_elenco_prodotti_vuoto_non_toglie_niente()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord> { Con("1", "a"), Con("2", "b") };
|
||||||
|
|
||||||
|
var kept = StatsMaintenance.Apply(list,
|
||||||
|
new StatsMaintenance.CleanupOptions { RemoveProductKeys = new HashSet<string>() });
|
||||||
|
|
||||||
|
Assert.Equal(2, kept.Count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modalita' della sessione. Due proprieta' da difendere: che Gara tolga esattamente il
|
||||||
|
/// lavoro dichiarato e nient'altro, e che la scelta sia immutabile per tutta la vita del
|
||||||
|
/// processo — un salvataggio a meta' sessione non deve cambiare il motore sotto i piedi.
|
||||||
|
/// </summary>
|
||||||
|
public class RuntimeProfileTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Addestramento_registra_tutto()
|
||||||
|
{
|
||||||
|
RuntimeProfile.ResetForTests();
|
||||||
|
RuntimeProfile.Initialize(RuntimeProfile.Addestramento);
|
||||||
|
|
||||||
|
Assert.False(RuntimeProfile.IsRace);
|
||||||
|
Assert.True(RuntimeProfile.RawPolls);
|
||||||
|
Assert.True(RuntimeProfile.VerboseAuctionLog);
|
||||||
|
Assert.True(RuntimeProfile.DetailedStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Gara_toglie_solo_il_lavoro_dichiarato()
|
||||||
|
{
|
||||||
|
RuntimeProfile.ResetForTests();
|
||||||
|
RuntimeProfile.Initialize(RuntimeProfile.Gara);
|
||||||
|
|
||||||
|
Assert.True(RuntimeProfile.IsRace);
|
||||||
|
Assert.False(RuntimeProfile.RawPolls);
|
||||||
|
Assert.False(RuntimeProfile.VerboseAuctionLog);
|
||||||
|
Assert.False(RuntimeProfile.DetailedStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_prima_scelta_vince_per_tutta_la_sessione()
|
||||||
|
{
|
||||||
|
RuntimeProfile.ResetForTests();
|
||||||
|
RuntimeProfile.Initialize(RuntimeProfile.Gara);
|
||||||
|
|
||||||
|
// Un salvataggio delle impostazioni a meta' sessione passa di qui: non deve
|
||||||
|
// cambiare niente fino al riavvio.
|
||||||
|
RuntimeProfile.Initialize(RuntimeProfile.Addestramento);
|
||||||
|
|
||||||
|
Assert.True(RuntimeProfile.IsRace);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData("qualcosa")]
|
||||||
|
[InlineData("gara")]
|
||||||
|
public void Un_valore_ignoto_vale_Addestramento_e_le_maiuscole_non_contano(string? valore)
|
||||||
|
{
|
||||||
|
RuntimeProfile.ResetForTests();
|
||||||
|
RuntimeProfile.Initialize(valore);
|
||||||
|
|
||||||
|
// "gara" minuscolo e' Gara: un file modificato a mano non deve spegnere la scelta.
|
||||||
|
var atteso = string.Equals(valore, "gara", System.StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal(atteso, RuntimeProfile.IsRace);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Il_predefinito_di_fabbrica_e_Addestramento()
|
||||||
|
{
|
||||||
|
RuntimeProfile.ResetForTests();
|
||||||
|
Assert.Equal(RuntimeProfile.Addestramento, new AppSettings().SessionMode);
|
||||||
|
Assert.False(RuntimeProfile.IsRace);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ internal static class TestEnvironment
|
|||||||
{
|
{
|
||||||
var sandbox = Path.Combine(Path.GetTempPath(), "AutoBidder.Tests", Guid.NewGuid().ToString("N"));
|
var sandbox = Path.Combine(Path.GetTempPath(), "AutoBidder.Tests", Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
// Anche la radice della configurazione: senza, un test che tocca le impostazioni
|
||||||
|
// leggerebbe e scriverebbe quelle vere di chi lancia la suite.
|
||||||
|
AppPaths.OverrideConfigRootForTests(sandbox);
|
||||||
|
|
||||||
AppPaths.Configure(
|
AppPaths.Configure(
|
||||||
dataFolder: Path.Combine(sandbox, "Dati"),
|
dataFolder: Path.Combine(sandbox, "Dati"),
|
||||||
statsFolder: Path.Combine(sandbox, "Statistiche"));
|
statsFolder: Path.Combine(sandbox, "Statistiche"));
|
||||||
|
|||||||
@@ -10,26 +10,104 @@ namespace AutoBidder.Utilities
|
|||||||
/// <c>Local</c>: le aste finivano in una cartella e le impostazioni in un'altra, cosa
|
/// <c>Local</c>: le aste finivano in una cartella e le impostazioni in un'altra, cosa
|
||||||
/// che rendeva difficile perfino sapere dove guardare per un salvataggio.</para>
|
/// che rendeva difficile perfino sapere dove guardare per un salvataggio.</para>
|
||||||
///
|
///
|
||||||
/// <para>La struttura è deliberatamente su due livelli:</para>
|
/// <para>La struttura è deliberatamente su tre livelli:</para>
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><b>Radice fissa</b> (<c>%LocalAppData%\AutoBidder</c>): configurazione e
|
/// <item><b>Radice dell'autenticazione</b> (<c>%LocalAppData%\AutoBidder</c>): sessione,
|
||||||
/// sessione. Non si sposta, perché è lì che si legge <i>dove</i> stanno gli altri
|
/// riferimenti del sito e profilo del browser. Non si sposta mai: la sessione è
|
||||||
/// dati — spostarla creerebbe un problema dell'uovo e della gallina. La sessione
|
/// protetta con DPAPI e legata a questo utente Windows, quindi su un altro disco o
|
||||||
/// resta qui anche perché è protetta con DPAPI e legata a questo utente Windows:
|
/// in una cartella sincronizzata non sarebbe comunque leggibile.</item>
|
||||||
/// su un disco condiviso non sarebbe comunque leggibile altrove.</item>
|
/// <item><b>Radice della configurazione</b> (<c>Documenti\AutoBidder</c> per le
|
||||||
|
/// installazioni nuove): le impostazioni e, di serie, i dati. È una cartella nota a
|
||||||
|
/// Windows, quindi si trova senza dover leggere niente prima — che è l'unico requisito
|
||||||
|
/// per il file che dice <i>dove</i> stanno gli altri.</item>
|
||||||
/// <item><b>Cartella dati</b> (configurabile): aste monitorate, storico, prodotti
|
/// <item><b>Cartella dati</b> (configurabile): aste monitorate, storico, prodotti
|
||||||
/// seguiti e statistiche. È questa che si può portare su un altro disco, in una
|
/// seguiti e statistiche. Si può portare su un altro disco o accanto ai propri fogli
|
||||||
/// cartella sincronizzata o accanto ai propri fogli di calcolo.</item>
|
/// di calcolo.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
|
///
|
||||||
|
/// <para><b>Chi ha già un'installazione non viene spostato.</b> Se le impostazioni
|
||||||
|
/// esistono in <c>%LocalAppData%</c> e non in Documenti, si continua a usare quelle
|
||||||
|
/// dove sono: scelta esplicita dell'utente, per non toccare mesi di raccolta. Chi vuole
|
||||||
|
/// cambiare posto lo fa dalla scheda Impostazioni.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AppPaths
|
public static class AppPaths
|
||||||
{
|
{
|
||||||
/// <summary>Radice fissa: configurazione dell'applicazione e sessione.</summary>
|
/// <summary>
|
||||||
|
/// Radice dell'autenticazione: sessione, riferimenti del sito, profilo del browser.
|
||||||
|
/// Resta in <c>%LocalAppData%</c> qualunque cosa succeda alle altre cartelle.
|
||||||
|
/// </summary>
|
||||||
public static string Root { get; } = Path.Combine(
|
public static string Root { get; } = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
|
||||||
|
|
||||||
/// <summary>Impostazioni. Sempre nella radice fissa: contiene il percorso dei dati.</summary>
|
/// <summary>Radice della configurazione per chi installa da zero: Documenti.</summary>
|
||||||
public static string SettingsFile => Path.Combine(Root, "settings.json");
|
private static string DocumentsRoot
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var docs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||||
|
if (!string.IsNullOrWhiteSpace(docs)) return Path.Combine(docs, "AutoBidder");
|
||||||
|
}
|
||||||
|
catch { /* profilo senza Documenti: si ripiega sotto */ }
|
||||||
|
|
||||||
|
return Root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? _configRootOverride;
|
||||||
|
private static string? _configRootResolved;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Radice della configurazione: dove stanno le impostazioni e, di serie, i dati.
|
||||||
|
///
|
||||||
|
/// <para>Si decide una volta sola, al primo accesso, e non cambia più per tutta la
|
||||||
|
/// vita del processo — spostarla a metà lascerebbe metà degli archivi in una
|
||||||
|
/// cartella e metà nell'altra.</para>
|
||||||
|
///
|
||||||
|
/// <para>Ordine: Documenti se le impostazioni ci sono già; altrimenti
|
||||||
|
/// <c>%LocalAppData%</c> se ci sono lì (installazione precedente, che resta dov'è);
|
||||||
|
/// altrimenti Documenti, perché è un'installazione nuova.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static string ConfigRoot
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_configRootOverride != null) return _configRootOverride;
|
||||||
|
return _configRootResolved ??= ResolveConfigRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveConfigRoot()
|
||||||
|
{
|
||||||
|
var documenti = DocumentsRoot;
|
||||||
|
|
||||||
|
if (File.Exists(Path.Combine(documenti, "settings.json"))) return documenti;
|
||||||
|
if (File.Exists(Path.Combine(Root, "settings.json"))) return Root;
|
||||||
|
|
||||||
|
// Installazione nuova: Documenti, purché sia davvero scrivibile.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(documenti);
|
||||||
|
return documenti;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solo per i test: fissa la radice della configurazione senza guardare il disco.
|
||||||
|
/// Senza, la suite leggerebbe e scriverebbe le impostazioni vere di chi la lancia.
|
||||||
|
/// </summary>
|
||||||
|
public static void OverrideConfigRootForTests(string? path) => _configRootOverride = path;
|
||||||
|
|
||||||
|
/// <summary>Impostazioni: nella radice della configurazione, che si trova senza leggerle.</summary>
|
||||||
|
public static string SettingsFile => Path.Combine(ConfigRoot, "settings.json");
|
||||||
|
|
||||||
|
/// <summary>Copie di sicurezza dello storico, fatte prima di ogni pulizia.</summary>
|
||||||
|
public static string BackupsFolder => Path.Combine(ConfigRoot, "Backups");
|
||||||
|
|
||||||
/// <summary>Cookie di sessione protetto con DPAPI.</summary>
|
/// <summary>Cookie di sessione protetto con DPAPI.</summary>
|
||||||
public static string SessionFile => Path.Combine(Root, "session.dat");
|
public static string SessionFile => Path.Combine(Root, "session.dat");
|
||||||
@@ -55,7 +133,7 @@ namespace AutoBidder.Utilities
|
|||||||
/// Cartella dei dati d'esercizio: aste monitorate, storico, prodotti seguiti.
|
/// Cartella dei dati d'esercizio: aste monitorate, storico, prodotti seguiti.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string DataFolder =>
|
public static string DataFolder =>
|
||||||
_dataFolderOverride.Length > 0 ? _dataFolderOverride : Path.Combine(Root, "Dati");
|
_dataFolderOverride.Length > 0 ? _dataFolderOverride : Path.Combine(ConfigRoot, "Dati");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cartella delle statistiche e delle esportazioni. Separata dalla precedente
|
/// Cartella delle statistiche e delle esportazioni. Separata dalla precedente
|
||||||
@@ -149,6 +227,7 @@ namespace AutoBidder.Utilities
|
|||||||
public static void EnsureFolders()
|
public static void EnsureFolders()
|
||||||
{
|
{
|
||||||
TryCreate(Root);
|
TryCreate(Root);
|
||||||
|
TryCreate(ConfigRoot);
|
||||||
TryCreate(DataFolder);
|
TryCreate(DataFolder);
|
||||||
TryCreate(StatsFolder);
|
TryCreate(StatsFolder);
|
||||||
TryCreate(LogFolder);
|
TryCreate(LogFolder);
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ namespace AutoBidder.Utilities
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var cartella = Path.Combine(AppPaths.Root, "Backups");
|
var cartella = AppPaths.BackupsFolder;
|
||||||
Directory.CreateDirectory(cartella);
|
Directory.CreateDirectory(cartella);
|
||||||
|
|
||||||
var nome = $"completed-auctions_{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
var nome = $"completed-auctions_{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AutoBidder.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// La modalità della sessione, letta una volta all'avvio e immutabile fino alla chiusura.
|
||||||
|
///
|
||||||
|
/// <para><b>Addestramento</b> registra tutto: ogni interrogazione nel dossier, ogni
|
||||||
|
/// decisione della strategia nel registro dell'asta, il dettaglio esteso delle aste
|
||||||
|
/// concluse. È la modalità con cui si raccolgono i dati che poi servono a tarare le
|
||||||
|
/// regole — e i dossier sono la materia prima di ogni analisi fatta finora.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Gara</b> fa solo il lavoro che serve a puntare. Quello che toglie:</para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>la riga JSON per ogni interrogazione nel dossier — a 17 poll al secondo per
|
||||||
|
/// asta un'asta lunga vale decine di megabyte;</item>
|
||||||
|
/// <item>le righe di registro informative («✓ prezzo OK» e simili), composte a ogni
|
||||||
|
/// giro prima ancora di sapere se qualcuno le leggerà;</item>
|
||||||
|
/// <item>il dettaglio esteso di ogni asta conclusa.</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>Restano puntate, esiti, errori, lo storico delle aste concluse e tutto ciò che
|
||||||
|
/// alimenta le decisioni — il riconoscimento del duello con l'autopuntata compreso, che
|
||||||
|
/// vive sulle puntate avversarie e non sui poll.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Cosa aspettarsi, misurato.</b> Il lavoro che Gara evita costa 4,9 µs per
|
||||||
|
/// interrogazione (3,3 la riga JSON, 1,6 le sei stringhe di registro). A 57 aste per
|
||||||
|
/// 17 poll al secondo sono 4,7 ms di CPU al secondo: mezzo punto percentuale di un
|
||||||
|
/// core. Il guadagno vero di Gara è sul disco e sulla leggibilità dei dossier, non
|
||||||
|
/// sulla velocità: la scrittura era già asincrona, e la CPU spesa a prepararla è
|
||||||
|
/// trascurabile. Se il programma è lento, la causa sta altrove.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Perché immutabile.</b> Scelta esplicita dell'utente: la modalità vale per
|
||||||
|
/// tutta la sessione e cambiarla richiede il riavvio. Un interruttore a caldo
|
||||||
|
/// funzionerebbe uguale, ma una sessione tutta di un tipo ha un pregio suo — i dossier
|
||||||
|
/// che ne escono sono confrontabili fra loro, senza buchi a metà.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class RuntimeProfile
|
||||||
|
{
|
||||||
|
public const string Addestramento = "Addestramento";
|
||||||
|
public const string Gara = "Gara";
|
||||||
|
|
||||||
|
private static string? _mode;
|
||||||
|
|
||||||
|
/// <summary>La modalità in vigore. Se nessuno l'ha fissata, Addestramento.</summary>
|
||||||
|
public static string Mode => _mode ?? Addestramento;
|
||||||
|
|
||||||
|
public static bool IsRace => string.Equals(Mode, Gara, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fissa la modalità per tutta la vita del processo. La prima chiamata vince: le
|
||||||
|
/// successive sono ignorate, così un salvataggio delle impostazioni a metà sessione
|
||||||
|
/// non cambia il comportamento sotto i piedi del motore.
|
||||||
|
/// </summary>
|
||||||
|
public static void Initialize(string? mode)
|
||||||
|
{
|
||||||
|
if (_mode != null) return;
|
||||||
|
_mode = Normalize(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Il nome canonico di una modalità; qualunque altra cosa vale Addestramento.</summary>
|
||||||
|
public static string Normalize(string? mode) =>
|
||||||
|
string.Equals(mode, Gara, StringComparison.OrdinalIgnoreCase) ? Gara : Addestramento;
|
||||||
|
|
||||||
|
/// <summary>Solo per i test: azzera la scelta.</summary>
|
||||||
|
public static void ResetForTests() => _mode = null;
|
||||||
|
|
||||||
|
// ── Cosa fa ciascuna modalità ────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Scrivere nel dossier una riga per ogni interrogazione.</summary>
|
||||||
|
public static bool RawPolls => !IsRace;
|
||||||
|
|
||||||
|
/// <summary>Comporre le righe di registro informative («✓ prezzo OK» e simili).</summary>
|
||||||
|
public static bool VerboseAuctionLog => !IsRace;
|
||||||
|
|
||||||
|
/// <summary>Scrivere il dettaglio esteso di ogni asta conclusa.</summary>
|
||||||
|
public static bool DetailedStats => !IsRace;
|
||||||
|
|
||||||
|
/// <summary>Una riga per il registro d'avvio.</summary>
|
||||||
|
public static string Describe() => IsRace
|
||||||
|
? "Gara: niente poll grezzi nel dossier, niente registro informativo, niente dettaglio esteso"
|
||||||
|
: "Addestramento: registrazione completa";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -565,6 +565,13 @@ namespace AutoBidder.Utilities
|
|||||||
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
|
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
|
||||||
// doppio che fra le 10 e le 13.
|
// doppio che fra le 10 e le 13.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modalità della sessione: <c>Addestramento</c> registra tutto, <c>Gara</c> fa solo
|
||||||
|
/// il lavoro che serve a puntare. Letta una volta all'avvio: cambiarla richiede il
|
||||||
|
/// riavvio. Vedi <see cref="RuntimeProfile"/> per cosa cambia e per quanto pesa.
|
||||||
|
/// </summary>
|
||||||
|
public string SessionMode { get; set; } = RuntimeProfile.Addestramento;
|
||||||
|
|
||||||
// ── Rimozione automatica delle aste concluse ─────────────────────
|
// ── Rimozione automatica delle aste concluse ─────────────────────
|
||||||
// Vedi FinishedAuctionCleanup: si toglie di serie, si trattiene cio' su cui c'e'
|
// Vedi FinishedAuctionCleanup: si toglie di serie, si trattiene cio' su cui c'e'
|
||||||
// stato un esborso.
|
// stato un esborso.
|
||||||
@@ -849,7 +856,7 @@ namespace AutoBidder.Utilities
|
|||||||
{
|
{
|
||||||
// Le impostazioni stanno nella radice fissa: contengono il percorso degli altri
|
// Le impostazioni stanno nella radice fissa: contengono il percorso degli altri
|
||||||
// dati, quindi non possono a loro volta dipendere da quel percorso.
|
// dati, quindi non possono a loro volta dipendere da quel percorso.
|
||||||
private static string _folder => AppPaths.Root;
|
private static string _folder => AppPaths.ConfigRoot;
|
||||||
private static string _file => AppPaths.SettingsFile;
|
private static string _file => AppPaths.SettingsFile;
|
||||||
|
|
||||||
private static readonly object _reloadLock = new();
|
private static readonly object _reloadLock = new();
|
||||||
|
|||||||
@@ -113,6 +113,40 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
||||||
public DateTime? OlderThan { get; set; }
|
public DateTime? OlderThan { get; set; }
|
||||||
|
|
||||||
|
// ── Per prodotto, per esborso, per anomalia ──────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chiavi prodotto da togliere per intero. Vuoto o null = nessun filtro.
|
||||||
|
/// Serve quando un prodotto è sparito dal sito o non interessa più: le sue
|
||||||
|
/// aste non alimentano nessuna decisione e pesano solo sulle medie globali.
|
||||||
|
/// Il confronto è senza distinzione di maiuscole: la chiave è già normalizzata,
|
||||||
|
/// ma un file modificato a mano può non esserlo.
|
||||||
|
/// </summary>
|
||||||
|
public HashSet<string>? RemoveProductKeys { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Togliere le aste solo osservate, cioè senza nessuna puntata nostra. Tiene
|
||||||
|
/// quelle su cui si è speso: sono le uniche col costo reale dentro.
|
||||||
|
/// </summary>
|
||||||
|
public bool RemoveObservedOnly { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Togliere le aste su cui si è puntato. È il verso opposto: serve a chi vuole
|
||||||
|
/// uno storico «di mercato» non contaminato dalle proprie mosse. Un'asta in cui
|
||||||
|
/// si è intervenuti ha un prezzo finale che si è contribuito a fare, e per
|
||||||
|
/// stimare cosa succede senza di noi va tolta.
|
||||||
|
/// </summary>
|
||||||
|
public bool RemoveWithMyBids { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Togliere le aste chiuse oltre questa percentuale del valore del prodotto.
|
||||||
|
/// Null = nessun limite. Una chiusura al 60% del valore su un buono da 100 €
|
||||||
|
/// non descrive il mercato: descrive due persone che si sono incaponite.
|
||||||
|
/// Tenerla sposta le medie e i limiti consigliati verso l'alto per tutti.
|
||||||
|
/// Senza valore noto l'asta resta: non si può dire, e non si indovina.
|
||||||
|
/// </summary>
|
||||||
|
public double? MaxPriceRatioPercent { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Quante righe cadrebbero, e per quale motivo.</summary>
|
/// <summary>Quante righe cadrebbero, e per quale motivo.</summary>
|
||||||
@@ -183,6 +217,20 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
||||||
Mark(r, "più vecchia del limite");
|
Mark(r, "più vecchia del limite");
|
||||||
|
|
||||||
|
if (options.RemoveProductKeys is { Count: > 0 } chiavi &&
|
||||||
|
!string.IsNullOrWhiteSpace(r.ProductKey) && chiavi.Contains(r.ProductKey))
|
||||||
|
Mark(r, "prodotto scelto");
|
||||||
|
|
||||||
|
if (options.RemoveObservedOnly && r.MyBidsUsed <= 0)
|
||||||
|
Mark(r, "solo osservata");
|
||||||
|
|
||||||
|
if (options.RemoveWithMyBids && r.MyBidsUsed > 0)
|
||||||
|
Mark(r, "con mie puntate");
|
||||||
|
|
||||||
|
if (options.MaxPriceRatioPercent is { } tetto && r.BuyNowPrice is > 0 &&
|
||||||
|
r.FinalPrice / r.BuyNowPrice.Value * 100.0 > tetto)
|
||||||
|
Mark(r, "prezzo finale fuori scala");
|
||||||
}
|
}
|
||||||
|
|
||||||
return doomed;
|
return doomed;
|
||||||
|
|||||||
Reference in New Issue
Block a user