Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d578debe2 |
@@ -60,6 +60,7 @@
|
|||||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
ToolTip="Ferma tutte: nessuna richiesta"
|
ToolTip="Ferma tutte: nessuna richiesta"
|
||||||
Click="StopButton_Click">
|
Click="StopButton_Click">
|
||||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
@@ -390,6 +391,7 @@
|
|||||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Style="{StaticResource MiniIconButton}"
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
ToolTip="Ferma"
|
ToolTip="Ferma"
|
||||||
IsEnabled="{Binding CanStop}"
|
IsEnabled="{Binding CanStop}"
|
||||||
Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
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 (€)"
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Costo medio di una puntata (€)"
|
||||||
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
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"
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
||||||
Text="0,20" Margin="10,10"/>
|
Text="0,20" Margin="10,10"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -46,6 +46,17 @@
|
|||||||
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"/>
|
||||||
|
|
||||||
|
<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">
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
||||||
Content="Aste chiuse da più di"
|
Content="Aste chiuse da più di"
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ namespace AutoBidder.Dialogs
|
|||||||
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
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 &&
|
if (ChkOlderThan.IsChecked == true &&
|
||||||
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
Cose da fare:
|
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 failed = 0;
|
||||||
var skipped = 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++)
|
for (var i = 0; i < missing.Count; i++)
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested) break;
|
if (ct.IsCancellationRequested) break;
|
||||||
@@ -78,15 +98,19 @@ namespace AutoBidder.Services
|
|||||||
var check = AuctionIntegrity.CheckWinnerBids(record);
|
var check = AuctionIntegrity.CheckWinnerBids(record);
|
||||||
if (check.IsTrustworthy)
|
if (check.IsTrustworthy)
|
||||||
{
|
{
|
||||||
// Si riscrive solo il record toccato: Append sostituisce
|
// Va nel blocco: AppendMany sostituisce i record con lo
|
||||||
// quello con lo stesso id.
|
// stesso id, quindi il risultato è identico a un Append
|
||||||
CompletedAuctionsStore.Append(record);
|
// per ciascuno, con un salvataggio invece di cinquanta.
|
||||||
|
daSalvare.Add(record);
|
||||||
|
if (daSalvare.Count >= DimensioneBlocco) Deposita();
|
||||||
|
|
||||||
updated++;
|
updated++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Il valore c'è ma non regge il confronto col prezzo: non si
|
// Il valore c'è ma non regge il confronto col prezzo: non
|
||||||
// salva, e si dice perché. Meglio un buco che una media storta.
|
// si salva, e si dice perché. Meglio un buco che una media
|
||||||
|
// storta.
|
||||||
record.WinnerBidsPaid = null;
|
record.WinnerBidsPaid = null;
|
||||||
record.WinnerBidsFree = null;
|
record.WinnerBidsFree = null;
|
||||||
skipped++;
|
skipped++;
|
||||||
@@ -108,6 +132,14 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
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);
|
return new Result(missing.Count, updated, failed, skipped);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,4 +309,73 @@ public class StatsMaintenanceTests
|
|||||||
Assert.Equal(report.Kept, kept.Count);
|
Assert.Equal(report.Kept, kept.Count);
|
||||||
Assert.Equal(report.Removed, list.Count - 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="AutoGenerateColumns" Value="False"/>
|
||||||
<Setter Property="SelectionMode" Value="Single"/>
|
<Setter Property="SelectionMode" Value="Single"/>
|
||||||
<Setter Property="EnableRowVirtualization" Value="True"/>
|
<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="VerticalGridLinesBrush" Value="Transparent"/>
|
||||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@@ -63,5 +63,33 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
||||||
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
|
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 };
|
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>
|
/// <summary>Percorso del file storico (per mostrarlo/aprirlo nella UI).</summary>
|
||||||
public static string StorePath => FilePath;
|
public static string StorePath => FilePath;
|
||||||
|
|
||||||
@@ -34,16 +53,50 @@ namespace AutoBidder.Utilities
|
|||||||
public static void Append(CompletedAuctionRecord record)
|
public static void Append(CompletedAuctionRecord record)
|
||||||
{
|
{
|
||||||
if (record == null) return;
|
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)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var list = LoadInternal();
|
var list = LoadInternal();
|
||||||
|
|
||||||
var existing = list.FindIndex(r =>
|
// Indice per id: cercare ogni record con un FindIndex renderebbe
|
||||||
!string.IsNullOrEmpty(r.AuctionId) && r.AuctionId == record.AuctionId);
|
// 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;
|
foreach (var record in records)
|
||||||
else list.Add(record);
|
{
|
||||||
|
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);
|
SaveInternal(list);
|
||||||
}
|
}
|
||||||
@@ -119,10 +172,12 @@ namespace AutoBidder.Utilities
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(FilePath)) File.Delete(FilePath);
|
if (File.Exists(FilePath)) File.Delete(FilePath);
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
|
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,18 +276,33 @@ namespace AutoBidder.Utilities
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(FilePath))
|
if (!File.Exists(FilePath))
|
||||||
|
{
|
||||||
|
_cache = null;
|
||||||
return new List<CompletedAuctionRecord>();
|
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);
|
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>();
|
?? new List<CompletedAuctionRecord>();
|
||||||
|
|
||||||
|
_cache = list;
|
||||||
|
_cacheStamp = stamp;
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore lettura storico: {ex.Message}");
|
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>();
|
return new List<CompletedAuctionRecord>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,10 +316,16 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
var json = JsonSerializer.Serialize(list, JsonOptions);
|
var json = JsonSerializer.Serialize(list, JsonOptions);
|
||||||
File.WriteAllText(FilePath, json);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore salvataggio storico: {ex.Message}");
|
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>
|
/// <summary>Doppioni con lo stesso id: si tiene il più recente.</summary>
|
||||||
public bool RemoveDuplicates { get; set; } = true;
|
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>
|
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
||||||
public DateTime? OlderThan { get; set; }
|
public DateTime? OlderThan { get; set; }
|
||||||
}
|
}
|
||||||
@@ -161,6 +172,15 @@ namespace AutoBidder.Utilities
|
|||||||
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
|
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
|
||||||
Mark(r, "senza valore del prodotto");
|
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)
|
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
||||||
Mark(r, "più vecchia del limite");
|
Mark(r, "più vecchia del limite");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ DisableProgramGroupPage=yes
|
|||||||
DisableDirPage=auto
|
DisableDirPage=auto
|
||||||
|
|
||||||
OutputDir={#OutputDir}
|
OutputDir={#OutputDir}
|
||||||
OutputBaseFilename=AutoBidder-{#AppVersion}-setup
|
OutputBaseFilename=AutoBidder_{#AppVersion}
|
||||||
SetupIconFile=..\Icon\favicon.ico
|
SetupIconFile=..\Icon\favicon.ico
|
||||||
UninstallDisplayIcon={app}\{#AppExeName}
|
UninstallDisplayIcon={app}\{#AppExeName}
|
||||||
UninstallDisplayName={#AppName} {#AppVersion}
|
UninstallDisplayName={#AppName} {#AppVersion}
|
||||||
|
|||||||
@@ -465,7 +465,7 @@
|
|||||||
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Setup>$(CartellaPacchetti)\AutoBidder-$(V)-setup.exe</Setup>
|
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
||||||
@@ -540,7 +540,7 @@
|
|||||||
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
||||||
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
||||||
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
||||||
<Setup>$(CartellaPacchetti)\AutoBidder-$(V)-setup.exe</Setup>
|
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<MakeDir Directories="$(Tmp)" />
|
<MakeDir Directories="$(Tmp)" />
|
||||||
|
|||||||
Reference in New Issue
Block a user