diff --git a/Mimante/Controls/AuctionMonitorControl.xaml b/Mimante/Controls/AuctionMonitorControl.xaml
index 210e157..11a2705 100644
--- a/Mimante/Controls/AuctionMonitorControl.xaml
+++ b/Mimante/Controls/AuctionMonitorControl.xaml
@@ -60,6 +60,7 @@
+ ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo.
Il valore predefinito è 0,20 €, cioè il pacchetto meno vantaggioso in vendita su Bidoo: tenerlo così fa stimare per eccesso, e una stima per eccesso al massimo ti fa rinunciare a un affare, mentre una per difetto ti fa spendere più del valore del prodotto.
Se compri pacchetti più grandi, abbassalo al costo che paghi davvero."/>
diff --git a/Mimante/Dialogs/StatsCleanupDialog.xaml b/Mimante/Dialogs/StatsCleanupDialog.xaml
index 3e1617e..ea08136 100644
--- a/Mimante/Dialogs/StatsCleanupDialog.xaml
+++ b/Mimante/Dialogs/StatsCleanupDialog.xaml
@@ -46,6 +46,17 @@
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"/>
+
+
+
+
+
+
0 and <= 100)
+ {
+ options.MinObservedCoverage = percento / 100.0;
+ }
+
if (ChkOlderThan.IsChecked == true &&
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
{
diff --git a/Mimante/Modifiche.txt b/Mimante/Modifiche.txt
index 784c42f..90c531e 100644
--- a/Mimante/Modifiche.txt
+++ b/Mimante/Modifiche.txt
@@ -1,2 +1,8 @@
Cose da fare:
- - Nulla per ora
\ No newline at end of file
+ - Non caricare i sorgenti del programma nella release. Deve solo essere presente la versione portable e la versione installabile.
+ - Il nome del setup deve essere AutoBidder_Versione.exe (ad esempio AutoBidder_4.14.0.exe)
+ - Colora il pulsante stop di rosso (così come quello verde è verde) per quanto riguarda i globali. Colora anche quelli relativi alla singola asta presenti sulla griglia di monitoraggio AutoBidder_Versione
+ - Verifica tutta l'applicazione e tutto il codice per cercare di ottimizzare l'applicazione per renderla più veloce e meno bisognosa di risorse
+ - Aggiungi la possibilità di pulire le aste non valide o con non abbastanza dati, come per esempio quelle non seguite completamente dall'inizio alla fine. In generale devo poterle pulire
+ - Agiungi una impostazione per indicare il costo di una puntata (fisso a 20 centesimi, che corrisponde al piano meno vantaggioso per l'acquisto di puntate dal sito di Bidoo)
+
\ No newline at end of file
diff --git a/Mimante/Services/WinnerBidsBackfill.cs b/Mimante/Services/WinnerBidsBackfill.cs
index ee31734..874b313 100644
--- a/Mimante/Services/WinnerBidsBackfill.cs
+++ b/Mimante/Services/WinnerBidsBackfill.cs
@@ -56,57 +56,89 @@ namespace AutoBidder.Services
var failed = 0;
var skipped = 0;
- for (var i = 0; i < missing.Count; i++)
+ // ── Perché si salva a blocchi ────────────────────────────────
+ // Un Append per asta rilegge e riscrive l'intero storico ogni volta:
+ // su un recupero completo (4675 aste) erano minuti di solo rimescolamento
+ // JSON e svariati gigabyte scritti sul disco. Un blocco ogni cinquanta
+ // taglia i salvataggi di cinquanta volte senza mettere a rischio più di
+ // cinquanta richieste già pagate: quello che non è ancora stato salvato
+ // viene comunque scritto all'uscita, annullamento compreso.
+ const int DimensioneBlocco = 50;
+ var daSalvare = new List(DimensioneBlocco);
+
+ void Deposita()
{
- if (ct.IsCancellationRequested) break;
+ if (daSalvare.Count == 0) return;
- var record = missing[i];
+ CompletedAuctionsStore.AppendMany(daSalvare);
+ daSalvare.Clear();
+ }
- try
+ try
+ {
+ for (var i = 0; i < missing.Count; i++)
{
- var state = await FetchAsync(record.AuctionId, ct).ConfigureAwait(false);
+ if (ct.IsCancellationRequested) break;
- if (state?.WinnerBidsTotal is null)
- {
- skipped++;
- }
- else
- {
- record.WinnerBidsPaid = state.WinnerBidsPaid;
- record.WinnerBidsFree = state.WinnerBidsFree;
+ var record = missing[i];
- var check = AuctionIntegrity.CheckWinnerBids(record);
- if (check.IsTrustworthy)
+ try
+ {
+ var state = await FetchAsync(record.AuctionId, ct).ConfigureAwait(false);
+
+ if (state?.WinnerBidsTotal is null)
{
- // Si riscrive solo il record toccato: Append sostituisce
- // quello con lo stesso id.
- CompletedAuctionsStore.Append(record);
- updated++;
+ skipped++;
}
else
{
- // Il valore c'è ma non regge il confronto col prezzo: non si
- // salva, e si dice perché. Meglio un buco che una media storta.
- record.WinnerBidsPaid = null;
- record.WinnerBidsFree = null;
- skipped++;
- OnLog?.Invoke($"[RECUPERO] {record.Name}: scartato ({check.Reason})");
+ record.WinnerBidsPaid = state.WinnerBidsPaid;
+ record.WinnerBidsFree = state.WinnerBidsFree;
+
+ var check = AuctionIntegrity.CheckWinnerBids(record);
+ if (check.IsTrustworthy)
+ {
+ // Va nel blocco: AppendMany sostituisce i record con lo
+ // stesso id, quindi il risultato è identico a un Append
+ // per ciascuno, con un salvataggio invece di cinquanta.
+ daSalvare.Add(record);
+ if (daSalvare.Count >= DimensioneBlocco) Deposita();
+
+ updated++;
+ }
+ else
+ {
+ // Il valore c'è ma non regge il confronto col prezzo: non
+ // si salva, e si dice perché. Meglio un buco che una media
+ // storta.
+ record.WinnerBidsPaid = null;
+ record.WinnerBidsFree = null;
+ skipped++;
+ OnLog?.Invoke($"[RECUPERO] {record.Name}: scartato ({check.Reason})");
+ }
}
}
- }
- catch (OperationCanceledException)
- {
- break;
- }
- catch (Exception ex)
- {
- failed++;
- OnLog?.Invoke($"[RECUPERO] {record.AuctionId}: {ex.Message}");
- }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ failed++;
+ OnLog?.Invoke($"[RECUPERO] {record.AuctionId}: {ex.Message}");
+ }
- OnProgress?.Invoke(i + 1, missing.Count, updated);
+ OnProgress?.Invoke(i + 1, missing.Count, updated);
- if (!await Wait.DelayAsync(DelayBetweenRequestsMs, ct).ConfigureAwait(false)) break;
+ if (!await Wait.DelayAsync(DelayBetweenRequestsMs, ct).ConfigureAwait(false)) break;
+ }
+ }
+ finally
+ {
+ // Anche se si esce per annullamento o per un errore: le richieste già
+ // fatte sono state pagate al server, buttarne i risultati significa
+ // rifarle al giro dopo.
+ Deposita();
}
return new Result(missing.Count, updated, failed, skipped);
diff --git a/Mimante/Tests/BidBudgetTests.cs b/Mimante/Tests/BidBudgetTests.cs
index 6f43dfd..04e2c16 100644
--- a/Mimante/Tests/BidBudgetTests.cs
+++ b/Mimante/Tests/BidBudgetTests.cs
@@ -309,4 +309,73 @@ public class StatsMaintenanceTests
Assert.Equal(report.Kept, kept.Count);
Assert.Equal(report.Removed, list.Count - kept.Count);
}
+
+ // ── Aste seguite solo in parte ───────────────────────────────────────
+ //
+ // Il prezzo finale in centesimi e' il totale delle puntate dell'asta: confrontarlo
+ // con i reset osservati dice quanta asta si e' vista, senza bisogno di registrare
+ // nulla di nuovo. Vale quindi anche sullo storico raccolto prima della regola.
+
+ private static CompletedAuctionRecord Seguita(string id, double price, int resets)
+ {
+ var r = R(id: id, price: price, bidsFree: 1);
+ r.TotalResets = resets;
+ return r;
+ }
+
+ [Theory]
+ [InlineData(1.00, 100, 1.0)] // 1,00 EUR = 100 puntate, viste tutte
+ [InlineData(1.00, 50, 0.5)] // agganciata a meta'
+ [InlineData(1.00, 0, 0.0)] // vista solo la fine
+ public void La_copertura_e_i_reset_sul_totale_di_puntate(
+ double price, int resets, double attesa)
+ {
+ Assert.Equal(attesa, AuctionIntegrity.ObservedCoverage(price, resets)!.Value, 3);
+ }
+
+ [Fact]
+ public void Senza_prezzo_la_copertura_non_si_puo_dire()
+ {
+ Assert.Null(AuctionIntegrity.ObservedCoverage(0, 42));
+ }
+
+ [Fact]
+ public void La_copertura_non_supera_mai_uno()
+ {
+ // L'arrotondamento del prezzo puo' produrre qualche reset di troppo: non e'
+ // un'asta osservata piu' del possibile, e non deve diventare 103%.
+ Assert.Equal(1.0, AuctionIntegrity.ObservedCoverage(1.00, 103)!.Value, 3);
+ }
+
+ [Fact]
+ public void Toglie_le_aste_seguite_sotto_la_soglia()
+ {
+ var list = new List
+ {
+ Seguita("intera", 1.00, 100),
+ Seguita("quasi", 1.00, 95),
+ Seguita("meta", 1.00, 50),
+ };
+
+ var options = new StatsMaintenance.CleanupOptions
+ {
+ MinObservedCoverage = 0.90
+ };
+
+ var kept = StatsMaintenance.Apply(list, options);
+
+ Assert.Equal(2, kept.Count);
+ Assert.DoesNotContain(kept, r => r.AuctionId == "meta");
+ }
+
+ [Fact]
+ public void Spenta_la_soglia_non_toglie_nulla_per_copertura()
+ {
+ var list = new List { Seguita("meta", 1.00, 1) };
+
+ var report = StatsMaintenance.Preview(list,
+ new StatsMaintenance.CleanupOptions { MinObservedCoverage = null });
+
+ Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys);
+ }
}
diff --git a/Mimante/Tests/CompletedAuctionsStoreTests.cs b/Mimante/Tests/CompletedAuctionsStoreTests.cs
new file mode 100644
index 0000000..fadd992
--- /dev/null
+++ b/Mimante/Tests/CompletedAuctionsStoreTests.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using AutoBidder.Models;
+using AutoBidder.Utilities;
+using Xunit;
+
+namespace AutoBidder.Tests;
+
+///
+/// Archivio delle aste concluse: copia in memoria e salvataggio a blocchi.
+///
+/// Sono le due modifiche che hanno tolto dal percorso caldo la rilettura dell'intero
+/// file — ~72 ms su 5876 record, pagati sul thread dell'interfaccia a ogni asta che finisce.
+/// La proprieta' da difendere e' che andare piu' veloce non cambi cosa viene salvato:
+/// AppendMany deve dare lo stesso risultato di tanti Append, e la copia in memoria non deve
+/// mai servire un contenuto che sul disco non c'e' piu'.
+///
+public class CompletedAuctionsStoreTests
+{
+ private static CompletedAuctionRecord R(string id, double price = 1.00, string? nome = null) =>
+ new()
+ {
+ AuctionId = id,
+ Name = nome ?? $"Asta {id}",
+ ProductKey = "prova",
+ FinalPrice = price,
+ Winner = "tizio",
+ EndedAt = DateTime.Now
+ };
+
+ private static void Svuota() => CompletedAuctionsStore.ReplaceAll(new List());
+
+ [Fact]
+ public void Append_sostituisce_quella_con_lo_stesso_id()
+ {
+ Svuota();
+
+ CompletedAuctionsStore.Append(R("100", price: 1.00));
+ CompletedAuctionsStore.Append(R("100", price: 2.50));
+
+ var tutte = CompletedAuctionsStore.LoadAll();
+
+ Assert.Single(tutte);
+ Assert.Equal(2.50, tutte[0].FinalPrice);
+ }
+
+ [Fact]
+ public void AppendMany_da_lo_stesso_risultato_di_tanti_Append()
+ {
+ var lotto = new[] { R("a", 1), R("b", 2), R("c", 3) };
+
+ Svuota();
+ foreach (var r in lotto) CompletedAuctionsStore.Append(r);
+ var unoAllaVolta = CompletedAuctionsStore.LoadAll()
+ .OrderBy(r => r.AuctionId).Select(r => (r.AuctionId, r.FinalPrice)).ToList();
+
+ Svuota();
+ CompletedAuctionsStore.AppendMany(lotto);
+ var inBlocco = CompletedAuctionsStore.LoadAll()
+ .OrderBy(r => r.AuctionId).Select(r => (r.AuctionId, r.FinalPrice)).ToList();
+
+ Assert.Equal(unoAllaVolta, inBlocco);
+ }
+
+ [Fact]
+ public void AppendMany_sostituisce_e_aggiunge_nello_stesso_giro()
+ {
+ Svuota();
+ CompletedAuctionsStore.AppendMany(new[] { R("x", 1), R("y", 2) });
+
+ CompletedAuctionsStore.AppendMany(new[] { R("y", 9), R("z", 3) });
+
+ var tutte = CompletedAuctionsStore.LoadAll().ToDictionary(r => r.AuctionId, r => r.FinalPrice);
+
+ Assert.Equal(3, tutte.Count);
+ Assert.Equal(9, tutte["y"]);
+ Assert.Equal(1, tutte["x"]);
+ Assert.Equal(3, tutte["z"]);
+ }
+
+ [Fact]
+ public void AppendMany_senza_niente_da_fare_non_esplode()
+ {
+ Svuota();
+ CompletedAuctionsStore.AppendMany(Array.Empty());
+
+ Assert.Empty(CompletedAuctionsStore.LoadAll());
+ }
+
+ [Fact]
+ public void I_record_senza_id_restano_tutti()
+ {
+ // Un id vuoto non identifica niente: due record cosi' non sono lo stesso record,
+ // e accorparli farebbe sparire un'asta davvero conclusa.
+ Svuota();
+ CompletedAuctionsStore.AppendMany(new[] { R(""), R("") });
+
+ Assert.Equal(2, CompletedAuctionsStore.LoadAll().Count);
+ }
+
+ [Fact]
+ public void La_copia_in_memoria_si_accorge_se_il_file_cambia_da_fuori()
+ {
+ Svuota();
+ CompletedAuctionsStore.Append(R("uno"));
+ Assert.Single(CompletedAuctionsStore.LoadAll());
+
+ // Ripristino di un backup o modifica a mano: la copia non deve continuare a
+ // servire il contenuto vecchio. La data si forza per non dipendere dalla
+ // granularita' con cui il file system aggiorna l'ora di scrittura.
+ File.WriteAllText(CompletedAuctionsStore.StorePath,
+ JsonSerializer.Serialize(new List { R("due"), R("tre") }));
+ File.SetLastWriteTimeUtc(CompletedAuctionsStore.StorePath, DateTime.UtcNow.AddSeconds(5));
+
+ var dopo = CompletedAuctionsStore.LoadAll();
+
+ Assert.Equal(2, dopo.Count);
+ Assert.DoesNotContain(dopo, r => r.AuctionId == "uno");
+ }
+
+ [Fact]
+ public void Dopo_ReplaceAll_si_legge_quello_nuovo()
+ {
+ Svuota();
+ CompletedAuctionsStore.AppendMany(new[] { R("vecchia") });
+
+ CompletedAuctionsStore.ReplaceAll(new List { R("nuova") });
+
+ var tutte = CompletedAuctionsStore.LoadAll();
+
+ Assert.Single(tutte);
+ Assert.Equal("nuova", tutte[0].AuctionId);
+ }
+}
diff --git a/Mimante/Themes/Controls.xaml b/Mimante/Themes/Controls.xaml
index 152fb63..16ded0c 100644
--- a/Mimante/Themes/Controls.xaml
+++ b/Mimante/Themes/Controls.xaml
@@ -457,6 +457,12 @@
+
+
diff --git a/Mimante/Utilities/AuctionIntegrity.cs b/Mimante/Utilities/AuctionIntegrity.cs
index 5798a08..e6e25b2 100644
--- a/Mimante/Utilities/AuctionIntegrity.cs
+++ b/Mimante/Utilities/AuctionIntegrity.cs
@@ -63,5 +63,33 @@ namespace AutoBidder.Utilities
///
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
+
+ ///
+ /// Quanta parte dell'asta è stata davvero osservata, da 0 a 1. null se non
+ /// si può dire (prezzo finale mancante).
+ ///
+ /// Sfrutta la stessa identità del controllo qui sopra: il prezzo finale in
+ /// centesimi è il numero totale di puntate dell'asta. Confrontarlo con i reset
+ /// visti dice, senza bisogno di registrare nient'altro, se l'asta è stata seguita
+ /// dall'inizio o presa per strada. Un'asta agganciata a metà ha numeri veri ma
+ /// parziali — è il caso peggiore, perché sembra buona.
+ ///
+ /// Il valore è tagliato a 1: sui dati reali (5876 aste) nessun record supera
+ /// 1,02, e il poco che sfora è arrotondamento del prezzo, non un'asta osservata
+ /// più del possibile.
+ ///
+ public static double? ObservedCoverage(double finalPrice, int totalResets)
+ {
+ if (finalPrice <= 0) return null;
+
+ var totalBids = Math.Round(finalPrice * 100);
+ if (totalBids <= 0) return null;
+
+ return Math.Min(1.0, Math.Max(0, totalResets) / totalBids);
+ }
+
+ /// Comodità: la copertura di un'asta già registrata.
+ public static double? ObservedCoverage(CompletedAuctionRecord record) =>
+ record == null ? null : ObservedCoverage(record.FinalPrice, record.TotalResets);
}
}
diff --git a/Mimante/Utilities/CompletedAuctionsStore.cs b/Mimante/Utilities/CompletedAuctionsStore.cs
index 39f7526..046c4ed 100644
--- a/Mimante/Utilities/CompletedAuctionsStore.cs
+++ b/Mimante/Utilities/CompletedAuctionsStore.cs
@@ -23,6 +23,25 @@ namespace AutoBidder.Utilities
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
+ // ── Copia in memoria ─────────────────────────────────────────────
+ //
+ // Il file cresce di un record per asta e arriva in fretta a qualche megabyte:
+ // a 5876 aste, leggerlo e deserializzarlo costa ~72 ms misurati. Senza copia
+ // in memoria quel prezzo si paga a ogni apertura delle statistiche e a ogni
+ // asta che finisce — e quella chiamata arriva da un gestore di eventi, cioe'
+ // dal thread dell'interfaccia, che resta fermo per tutto il tempo.
+ //
+ // L'invalidazione guarda la data di modifica del file, non un tempo di
+ // scadenza: cosi' la copia resta giusta anche se il file viene sostituito da
+ // fuori — ripristino di un backup, modifica a mano — che qui capita davvero.
+ //
+ // Il prezzo da sapere: i record restituiti sono gli STESSI oggetti che stanno
+ // nella copia. Chi ne modifica uno sta modificando anche la copia, prima e
+ // indipendentemente dal salvataggio. Repair e il recupero puntate contano su
+ // questo; chi non lo vuole si faccia una copia del record.
+ private static List? _cache;
+ private static DateTime _cacheStamp;
+
/// Percorso del file storico (per mostrarlo/aprirlo nella UI).
public static string StorePath => FilePath;
@@ -34,16 +53,50 @@ namespace AutoBidder.Utilities
public static void Append(CompletedAuctionRecord record)
{
if (record == null) return;
+ AppendMany(new[] { record });
+ }
+
+ ///
+ /// Aggiunge o sostituisce piu' aste con un solo salvataggio.
+ ///
+ /// Serve a chi ne tocca tante di fila, come il recupero delle puntate dei
+ /// vincitori. Chiamare in un ciclo rilegge e riscrive tutto
+ /// il file a ogni giro: sulle 4675 aste di un recupero completo erano minuti di
+ /// solo rimescolamento JSON e svariati gigabyte scritti sul disco per nulla.
+ ///
+ public static void AppendMany(IReadOnlyCollection records)
+ {
+ if (records == null || records.Count == 0) return;
lock (_lock)
{
var list = LoadInternal();
- var existing = list.FindIndex(r =>
- !string.IsNullOrEmpty(r.AuctionId) && r.AuctionId == record.AuctionId);
+ // Indice per id: cercare ogni record con un FindIndex renderebbe
+ // quadratico proprio il caso per cui questo metodo esiste.
+ var byId = new Dictionary(list.Count, StringComparer.Ordinal);
+ for (var i = 0; i < list.Count; i++)
+ {
+ var id = list[i].AuctionId;
+ if (!string.IsNullOrEmpty(id)) byId[id] = i;
+ }
- if (existing >= 0) list[existing] = record;
- else list.Add(record);
+ foreach (var record in records)
+ {
+ if (record == null) continue;
+
+ var id = record.AuctionId;
+
+ if (!string.IsNullOrEmpty(id) && byId.TryGetValue(id, out var at))
+ {
+ list[at] = record;
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(id)) byId[id] = list.Count;
+ list.Add(record);
+ }
+ }
SaveInternal(list);
}
@@ -119,10 +172,12 @@ namespace AutoBidder.Utilities
try
{
if (File.Exists(FilePath)) File.Delete(FilePath);
+ _cache = null;
}
catch (Exception ex)
{
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
+ _cache = null;
}
}
}
@@ -221,18 +276,33 @@ namespace AutoBidder.Utilities
try
{
if (!File.Exists(FilePath))
+ {
+ _cache = null;
return new List();
+ }
+
+ // Una lettura di data di modifica costa microsecondi; la lettura piu'
+ // la deserializzazione che evita ne costa decine di millisecondi.
+ var stamp = File.GetLastWriteTimeUtc(FilePath);
+ if (_cache != null && stamp == _cacheStamp) return _cache;
var json = File.ReadAllText(FilePath);
- if (string.IsNullOrWhiteSpace(json))
- return new List();
- return JsonSerializer.Deserialize>(json)
- ?? new List();
+ var list = string.IsNullOrWhiteSpace(json)
+ ? new List()
+ : JsonSerializer.Deserialize>(json)
+ ?? new List();
+
+ _cache = list;
+ _cacheStamp = stamp;
+ return list;
}
catch (Exception ex)
{
Console.WriteLine($"[STATS] Errore lettura storico: {ex.Message}");
+
+ // Non si sa cosa ci sia sul disco: la prossima volta si rilegge.
+ _cache = null;
return new List();
}
}
@@ -246,10 +316,16 @@ namespace AutoBidder.Utilities
var json = JsonSerializer.Serialize(list, JsonOptions);
File.WriteAllText(FilePath, json);
+
+ // La copia in memoria e' esattamente quello che si e' appena scritto:
+ // tenerla evita di rileggere il file al primo uso successivo.
+ _cache = list;
+ _cacheStamp = File.GetLastWriteTimeUtc(FilePath);
}
catch (Exception ex)
{
Console.WriteLine($"[STATS] Errore salvataggio storico: {ex.Message}");
+ _cache = null;
}
}
}
diff --git a/Mimante/Utilities/StatsMaintenance.cs b/Mimante/Utilities/StatsMaintenance.cs
index 5fbe8d8..44e6b23 100644
--- a/Mimante/Utilities/StatsMaintenance.cs
+++ b/Mimante/Utilities/StatsMaintenance.cs
@@ -100,6 +100,17 @@ namespace AutoBidder.Utilities
/// Doppioni con lo stesso id: si tiene il più recente.
public bool RemoveDuplicates { get; set; } = true;
+ ///
+ /// Copertura minima richiesta, da 0 a 1: sotto questa soglia l'asta è stata
+ /// agganciata a metà e va tolta. Null = non filtrare sulla copertura.
+ ///
+ /// È il criterio per «non seguita dall'inizio alla fine». Non serve
+ /// nessun campo nuovo nei record: la copertura si ricava dal prezzo finale,
+ /// vedi . Vale
+ /// quindi anche per lo storico raccolto prima che questa regola esistesse.
+ ///
+ public double? MinObservedCoverage { get; set; }
+
/// Aste chiuse prima di questa data. Null = nessun limite.
public DateTime? OlderThan { get; set; }
}
@@ -161,6 +172,15 @@ namespace AutoBidder.Utilities
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
Mark(r, "senza valore del prodotto");
+ if (options.MinObservedCoverage is { } soglia)
+ {
+ // Copertura non calcolabile = asta senza prezzo finale: qui vale come
+ // "non seguita", perché non c'è modo di dire il contrario.
+ var copertura = AuctionIntegrity.ObservedCoverage(r);
+ if (copertura is null || copertura.Value < soglia)
+ Mark(r, "seguita solo in parte");
+ }
+
if (options.OlderThan is { } limite && r.EndedAt < limite)
Mark(r, "più vecchia del limite");
}
diff --git a/Mimante/build/AutoBidder.iss b/Mimante/build/AutoBidder.iss
index e780581..e7bb724 100644
--- a/Mimante/build/AutoBidder.iss
+++ b/Mimante/build/AutoBidder.iss
@@ -50,7 +50,7 @@ DisableProgramGroupPage=yes
DisableDirPage=auto
OutputDir={#OutputDir}
-OutputBaseFilename=AutoBidder-{#AppVersion}-setup
+OutputBaseFilename=AutoBidder_{#AppVersion}
SetupIconFile=..\Icon\favicon.ico
UninstallDisplayIcon={app}\{#AppExeName}
UninstallDisplayName={#AppName} {#AppVersion}
diff --git a/Mimante/build/Release.proj b/Mimante/build/Release.proj
index 3bde98a..715bc84 100644
--- a/Mimante/build/Release.proj
+++ b/Mimante/build/Release.proj
@@ -465,7 +465,7 @@
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
- $(CartellaPacchetti)\AutoBidder-$(V)-setup.exe
+ $(CartellaPacchetti)\AutoBidder_$(V).exe
@@ -540,7 +540,7 @@
$(Tmp)\curl.cfg$(Tmp)\release.json$(Tmp)\risposta.json
- $(CartellaPacchetti)\AutoBidder-$(V)-setup.exe
+ $(CartellaPacchetti)\AutoBidder_$(V).exe