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:
2026-09-07 18:39:19 +02:00
co-authored by Claude Fable 5.1
parent 4e835e220a
commit 08c5023f77
19 changed files with 675 additions and 26 deletions
+43
View File
@@ -46,6 +46,49 @@
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"/>
<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.&#10;&#10;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">
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
Content="Aste seguite per meno del"
@@ -20,10 +20,36 @@ namespace AutoBidder.Dialogs
{
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)
{
InitializeComponent();
_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();
}
@@ -62,6 +88,22 @@ namespace AutoBidder.Dialogs
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;
}
@@ -71,6 +113,14 @@ namespace AutoBidder.Dialogs
{
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());
SummaryText.Text = report.Removed == 0