Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d578debe2 |
@@ -60,6 +60,7 @@
|
||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||
</Button>
|
||||
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
||||
Foreground="{DynamicResource Brush.Danger}"
|
||||
ToolTip="Ferma tutte: nessuna richiesta"
|
||||
Click="StopButton_Click">
|
||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||
@@ -390,6 +391,7 @@
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||
</Button>
|
||||
<Button Style="{StaticResource MiniIconButton}"
|
||||
Foreground="{DynamicResource Brush.Danger}"
|
||||
ToolTip="Ferma"
|
||||
IsEnabled="{Binding CanStop}"
|
||||
Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Costo medio di una puntata (€)"
|
||||
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo."/>
|
||||
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."/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
||||
Text="0,20" Margin="10,10"/>
|
||||
</Grid>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
|
||||
Content="Aste seguite per meno del"
|
||||
ToolTip="Toglie le aste agganciate a metà. Sono il caso peggiore: i numeri sono veri ma parziali, quindi sembrano buone e abbassano le medie senza dare segnali. Quanta asta si è vista si ricava dal prezzo finale, senza bisogno di alcun dato in più: ogni puntata vale un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta, e i reset osservati dicono quanti se ne sono visti. Sul tuo storico, su 5876 aste: al 75% ne resterebbero circa 78 su 100, al 90% circa 64, al 95% circa 54, al 99% circa 34."
|
||||
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||
<TextBox x:Name="CoverageBox" Text="90" Width="50" Margin="8,0"
|
||||
VerticalAlignment="Center" TextChanged="Option_Changed"/>
|
||||
<TextBlock Text="% della loro durata" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
||||
Content="Aste chiuse da più di"
|
||||
|
||||
@@ -45,6 +45,17 @@ namespace AutoBidder.Dialogs
|
||||
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
||||
};
|
||||
|
||||
// Una soglia illeggibile o fuori scala non deve diventare "togli tutto":
|
||||
// il filtro resta spento finché il numero non ha senso.
|
||||
if (ChkCoverage.IsChecked == true &&
|
||||
double.TryParse(CoverageBox.Text.Trim().Replace(',', '.'),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var percento) &&
|
||||
percento is > 0 and <= 100)
|
||||
{
|
||||
options.MinObservedCoverage = percento / 100.0;
|
||||
}
|
||||
|
||||
if (ChkOlderThan.IsChecked == true &&
|
||||
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
||||
{
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
Cose da fare:
|
||||
- Nulla per ora
|
||||
- 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)
|
||||
|
||||
@@ -56,6 +56,26 @@ namespace AutoBidder.Services
|
||||
var failed = 0;
|
||||
var skipped = 0;
|
||||
|
||||
// ── 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<CompletedAuctionRecord>(DimensioneBlocco);
|
||||
|
||||
void Deposita()
|
||||
{
|
||||
if (daSalvare.Count == 0) return;
|
||||
|
||||
CompletedAuctionsStore.AppendMany(daSalvare);
|
||||
daSalvare.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < missing.Count; i++)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
@@ -78,15 +98,19 @@ namespace AutoBidder.Services
|
||||
var check = AuctionIntegrity.CheckWinnerBids(record);
|
||||
if (check.IsTrustworthy)
|
||||
{
|
||||
// Si riscrive solo il record toccato: Append sostituisce
|
||||
// quello con lo stesso id.
|
||||
CompletedAuctionsStore.Append(record);
|
||||
// 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.
|
||||
// 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++;
|
||||
@@ -108,6 +132,14 @@ namespace AutoBidder.Services
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<CompletedAuctionRecord>
|
||||
{
|
||||
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<CompletedAuctionRecord> { Seguita("meta", 1.00, 1) };
|
||||
|
||||
var report = StatsMaintenance.Preview(list,
|
||||
new StatsMaintenance.CleanupOptions { MinObservedCoverage = null });
|
||||
|
||||
Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Archivio delle aste concluse: copia in memoria e salvataggio a blocchi.
|
||||
///
|
||||
/// <para>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 <b>cosa</b> 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'.</para>
|
||||
/// </summary>
|
||||
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<CompletedAuctionRecord>());
|
||||
|
||||
[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<CompletedAuctionRecord>());
|
||||
|
||||
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<CompletedAuctionRecord> { 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<CompletedAuctionRecord> { R("nuova") });
|
||||
|
||||
var tutte = CompletedAuctionsStore.LoadAll();
|
||||
|
||||
Assert.Single(tutte);
|
||||
Assert.Equal("nuova", tutte[0].AuctionId);
|
||||
}
|
||||
}
|
||||
@@ -457,6 +457,12 @@
|
||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||
<Setter Property="SelectionMode" Value="Single"/>
|
||||
<Setter Property="EnableRowVirtualization" Value="True"/>
|
||||
<!-- Il riciclo riusa i contenitori di riga invece di ricrearli a ogni giro di
|
||||
scorrimento. Con la griglia del monitor, che si aggiorna piu' volte al
|
||||
secondo mentre le aste corrono, e' la differenza fra allocare righe nuove
|
||||
di continuo e non allocarne quasi nessuna. Il valore predefinito di WPF e'
|
||||
"Standard", cioe' senza riciclo. -->
|
||||
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
|
||||
<Setter Property="VerticalGridLinesBrush" Value="Transparent"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
||||
</Style>
|
||||
|
||||
@@ -63,5 +63,33 @@ namespace AutoBidder.Utilities
|
||||
/// </summary>
|
||||
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
||||
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
|
||||
|
||||
/// <summary>
|
||||
/// Quanta parte dell'asta è stata davvero osservata, da 0 a 1. <c>null</c> se non
|
||||
/// si può dire (prezzo finale mancante).
|
||||
///
|
||||
/// <para>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.</para>
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Comodità: la copertura di un'asta già registrata.</summary>
|
||||
public static double? ObservedCoverage(CompletedAuctionRecord record) =>
|
||||
record == null ? null : ObservedCoverage(record.FinalPrice, record.TotalResets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CompletedAuctionRecord>? _cache;
|
||||
private static DateTime _cacheStamp;
|
||||
|
||||
/// <summary>Percorso del file storico (per mostrarlo/aprirlo nella UI).</summary>
|
||||
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 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiunge o sostituisce piu' aste con <b>un solo</b> salvataggio.
|
||||
///
|
||||
/// <para>Serve a chi ne tocca tante di fila, come il recupero delle puntate dei
|
||||
/// vincitori. Chiamare <see cref="Append"/> 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.</para>
|
||||
/// </summary>
|
||||
public static void AppendMany(IReadOnlyCollection<CompletedAuctionRecord> 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<string, int>(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<CompletedAuctionRecord>();
|
||||
}
|
||||
|
||||
// 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<CompletedAuctionRecord>();
|
||||
|
||||
return JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
||||
var list = string.IsNullOrWhiteSpace(json)
|
||||
? new List<CompletedAuctionRecord>()
|
||||
: JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
||||
?? new List<CompletedAuctionRecord>();
|
||||
|
||||
_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<CompletedAuctionRecord>();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,17 @@ namespace AutoBidder.Utilities
|
||||
/// <summary>Doppioni con lo stesso id: si tiene il più recente.</summary>
|
||||
public bool RemoveDuplicates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Copertura minima richiesta, da 0 a 1: sotto questa soglia l'asta è stata
|
||||
/// agganciata a metà e va tolta. Null = non filtrare sulla copertura.
|
||||
///
|
||||
/// <para>È il criterio per «non seguita dall'inizio alla fine». Non serve
|
||||
/// nessun campo nuovo nei record: la copertura si ricava dal prezzo finale,
|
||||
/// vedi <see cref="AuctionIntegrity.ObservedCoverage(double,int)"/>. Vale
|
||||
/// quindi anche per lo storico raccolto prima che questa regola esistesse.</para>
|
||||
/// </summary>
|
||||
public double? MinObservedCoverage { get; set; }
|
||||
|
||||
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -465,7 +465,7 @@
|
||||
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Setup>$(CartellaPacchetti)\AutoBidder-$(V)-setup.exe</Setup>
|
||||
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||
</PropertyGroup>
|
||||
|
||||
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
||||
@@ -540,7 +540,7 @@
|
||||
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
||||
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
||||
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
||||
<Setup>$(CartellaPacchetti)\AutoBidder-$(V)-setup.exe</Setup>
|
||||
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(Tmp)" />
|
||||
|
||||
Reference in New Issue
Block a user