Compare commits
6
Commits
5adc4a6526
...
v4.16.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b9691d692 | ||
|
|
5bac29b0af | ||
|
|
9b94e2d405 | ||
|
|
20434ca273 | ||
|
|
feb39ee2de | ||
|
|
1d578debe2 |
@@ -1,4 +1,4 @@
|
||||
<UserControl x:Class="AutoBidder.Controls.AuctionMonitorControl"
|
||||
<UserControl x:Class="AutoBidder.Controls.AuctionMonitorControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -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}}"
|
||||
@@ -524,14 +526,18 @@
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo puntata (ms)"
|
||||
ToolTip="Quanti millisecondi prima della scadenza parte la puntata. È il parametro che decide quanto costa vincere: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato.
|
||||
|
||||
Dalla rigiocata di 2.095 aste concluse: sotto il secondo basta in mediana 1 puntata per asta, sotto i 2 s ne servono 15, sotto i 3 s 46. Ma sotto i 600 ms un picco di rete fa arrivare la puntata a giochi chiusi. Tienilo fra 600 e 2000 ms, e comunque sopra il tuo ping medio."
|
||||
Il 75,3% delle puntate avversarie e' automatica e scatta a 2 secondi esatti: sotto quella soglia resta solo chi punta a mano, e una puntata manuale vince 3,6 volte piu' spesso di una automatica (178 puntate per vittoria contro 634, su 5410 aste).
|
||||
|
||||
Predefinito 500 ms: circa nove volte il ping misurato (47-78 ms). Piu' in basso non conviene, perche' Bidoo dichiara la scadenza al secondo intero."
|
||||
Style="{StaticResource FieldLabel}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="0" x:Name="SelectedBidBeforeDeadlineMs"
|
||||
Margin="0,0,10,2" Text="800"
|
||||
Margin="0,0,10,2" Text="500"
|
||||
TextChanged="SelectedBidBeforeDeadlineMs_TextChanged"
|
||||
ToolTip="Quanti millisecondi prima della scadenza parte la puntata. È il parametro che decide quanto costa vincere: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato.
|
||||
|
||||
Dalla rigiocata di 2.095 aste concluse: sotto il secondo basta in mediana 1 puntata per asta, sotto i 2 s ne servono 15, sotto i 3 s 46. Ma sotto i 600 ms un picco di rete fa arrivare la puntata a giochi chiusi. Tienilo fra 600 e 2000 ms, e comunque sopra il tuo ping medio."/>
|
||||
Il 75,3% delle puntate avversarie e' automatica e scatta a 2 secondi esatti: sotto quella soglia resta solo chi punta a mano, e una puntata manuale vince 3,6 volte piu' spesso di una automatica (178 puntate per vittoria contro 634, su 5410 aste).
|
||||
|
||||
Predefinito 500 ms: circa nove volte il ping misurato (47-78 ms). Piu' in basso non conviene, perche' Bidoo dichiara la scadenza al secondo intero."/>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="Prezzo minimo €"
|
||||
ToolTip="Sotto questo prezzo non puntare. Serve a lasciar salire un'asta prima di entrare. 0 = nessun limite."
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace AutoBidder
|
||||
public partial class MainWindow
|
||||
{
|
||||
private DispatcherTimer? _headerTimer;
|
||||
private DispatcherTimer? _timerTicker;
|
||||
|
||||
private void StartMonitorHeaderTimer()
|
||||
{
|
||||
@@ -25,9 +26,35 @@ namespace AutoBidder
|
||||
_headerTimer.Tick += (_, _) => RefreshMonitorHeader();
|
||||
_headerTimer.Start();
|
||||
|
||||
// ── Il conto alla rovescia scorre da solo ────────────────────────
|
||||
//
|
||||
// Il tempo mancante non ha bisogno di una risposta dal server per essere
|
||||
// ricalcolato: la scadenza è ancorata all'orologio di Bidoo e il resto è
|
||||
// sottrazione. Prima però veniva notificato solo all'arrivo di un poll, e
|
||||
// nelle prove reali fra un poll e l'altro passava anche un secondo e mezzo:
|
||||
// il numero restava fermo e sembrava tutto bloccato.
|
||||
//
|
||||
// Cinque battiti al secondo: la colonna mostra i decimi sotto il minuto,
|
||||
// quindi si vede scorrere davvero, e restano un ventesimo delle notifiche
|
||||
// che servirebbero per seguire il decimo esatto.
|
||||
_timerTicker = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
|
||||
_timerTicker.Tick += (_, _) => TickTimers();
|
||||
_timerTicker.Start();
|
||||
|
||||
RefreshMonitorHeader();
|
||||
}
|
||||
|
||||
private void TickTimers()
|
||||
{
|
||||
var vms = _auctionViewModels;
|
||||
|
||||
for (var i = 0; i < vms.Count; i++)
|
||||
{
|
||||
try { vms[i].RefreshTimeDisplay(); }
|
||||
catch { /* una riga che non si aggiorna non deve fermare le altre */ }
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshMonitorHeader()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -164,22 +164,28 @@ namespace AutoBidder.Engine
|
||||
private int NextPollDelay(AppSettings settings)
|
||||
{
|
||||
// Un'asta non ancora cominciata ha una cadenza tutta sua: vedi NotStartedDelay.
|
||||
// È l'unico caso rimasto in cui si aspetta, e non è una scelta di ritmo: fino
|
||||
// all'apertura il server non ha niente di diverso da dire, e interrogarlo
|
||||
// seicento volte al minuto per tre ore non anticipa l'inizio di un istante.
|
||||
var notStarted = NotStartedDelay(settings);
|
||||
if (notStarted > 0) return notStarted;
|
||||
|
||||
var remaining = _auction.EstimatedRemainingMs();
|
||||
|
||||
if (remaining == double.MaxValue) return settings.PollIntervalFarMs;
|
||||
|
||||
// In sola osservazione non c'è puntata da azzeccare: si risparmiano chiamate.
|
||||
var critical = _auction.State == RunState.Active
|
||||
? settings.PollIntervalCriticalMs
|
||||
: settings.PollIntervalNearMs;
|
||||
|
||||
if (remaining <= settings.CriticalWindowMs) return critical;
|
||||
if (remaining <= 10_000) return settings.PollIntervalNearMs;
|
||||
if (remaining <= 60_000) return settings.PollIntervalMidMs;
|
||||
return settings.PollIntervalFarMs;
|
||||
// ── Nessuna attesa: la cadenza la detta la rete ──────────────────────
|
||||
//
|
||||
// Prima qui c'erano quattro soglie configurabili (lontano / medio / vicino /
|
||||
// critico). Sui registri di tre giorni non hanno mai funzionato come scritto:
|
||||
// con 40 richieste al secondo di tetto globale spartite fra tutte le aste
|
||||
// seguite, i 220 ms della finestra critica diventavano 596 ms misurati, e con
|
||||
// il timer a un secondo restava un solo sguardo prima della scadenza.
|
||||
//
|
||||
// Il ritmo adesso è quello del giro di andata e ritorno: si riparte appena la
|
||||
// risposta è arrivata. Con ping ~57 ms misurato sono circa diciassette
|
||||
// interrogazioni al secondo per asta.
|
||||
//
|
||||
// Il freno non è più un numero scelto a tavolino ma la risposta del server:
|
||||
// vedi la reazione a 429 e 503 in PollLoopAsync. Se Bidoo dice di rallentare,
|
||||
// si rallenta; finché non lo dice, si va al massimo che regge la rete.
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -565,6 +565,47 @@ namespace AutoBidder.Models
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsInSoftRetreat { get; set; } = false;
|
||||
|
||||
// ── Duello con l'autopuntata avversaria ──────────────────────────
|
||||
// Vedi Utilities/AutoBidDuel per il perche' e per i numeri misurati.
|
||||
|
||||
/// <summary>Istante dell'ultima puntata nostra, per misurare quanto tarda la risposta.</summary>
|
||||
[JsonIgnore]
|
||||
public DateTime? LastMyBidAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Risposte avversarie di fila con la firma dell'autopuntata (~6 s). Oltre la
|
||||
/// soglia si smette di puntare su quest'asta: vedi <see cref="AutoBidDuelDetected"/>.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int AutoResponsesInARow { get; set; }
|
||||
|
||||
/// <summary>Riconosciuta un'autopuntata armata dall'altra parte.</summary>
|
||||
[JsonIgnore]
|
||||
public bool AutoBidDuelDetected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Registra la risposta avversaria arrivata adesso e aggiorna il conteggio.
|
||||
/// Restituisce true se proprio con questa risposta il duello viene riconosciuto,
|
||||
/// cosi' chi chiama puo' scriverlo nel registro una volta sola.
|
||||
/// </summary>
|
||||
public bool NoteOpponentResponse(DateTime nowUtc, int soglia)
|
||||
{
|
||||
if (LastMyBidAtUtc is not { } miaUltima) return false;
|
||||
|
||||
var ritardo = (nowUtc - miaUltima).TotalSeconds;
|
||||
|
||||
// Consumata: la prossima risposta va misurata dalla prossima puntata nostra.
|
||||
LastMyBidAtUtc = null;
|
||||
|
||||
AutoResponsesInARow = Utilities.AutoBidDuel.Aggiorna(AutoResponsesInARow, ritardo);
|
||||
|
||||
if (AutoBidDuelDetected) return false;
|
||||
if (!Utilities.AutoBidDuel.DuelloRiconosciuto(AutoResponsesInARow, soglia)) return false;
|
||||
|
||||
AutoBidDuelDetected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contatore puntate effettuate in questa sessione su questa asta
|
||||
@@ -662,6 +703,13 @@ namespace AutoBidder.Models
|
||||
/// Override: abilita/disabilita soft retreat per questa asta
|
||||
/// </summary>
|
||||
public bool? SoftRetreatEnabledOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Override per questa asta della sospensione a fascia oraria.
|
||||
/// <c>null</c> = come dice il prodotto, e se il prodotto tace come dicono le
|
||||
/// impostazioni globali. Vedi <see cref="Utilities.BiddingHours"/>.
|
||||
/// </summary>
|
||||
public bool? QuietHoursEnabledOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Override: limite puntate per questa asta
|
||||
|
||||
@@ -14,11 +14,37 @@ namespace AutoBidder.Models
|
||||
[JsonPropertyName("Price")]
|
||||
public decimal Price { get; set; }
|
||||
|
||||
/// <summary>Puntata fatta dall'autopuntata del sito: scatta a 2 secondi dalla fine.</summary>
|
||||
public const string TipoAuto = "Auto";
|
||||
|
||||
/// <summary>Puntata fatta a mano. Vale 3,6 volte una automatica: vedi <see cref="Utilities.AutoBidDuel"/>.</summary>
|
||||
public const string TipoManuale = "Manuale";
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di puntata (Auto/Manuale)
|
||||
/// Il tipo non lo ha detto il server.
|
||||
///
|
||||
/// <para>Serve perché non tutte le puntate in elenco vengono dallo storico di
|
||||
/// <c>data.php</c>: quando il motore si accorge di un cambio di prezzo fra due
|
||||
/// interrogazioni ne ricostruisce una da sé, e di quella il tipo non lo sa. Prima
|
||||
/// venivano marcate "Auto" comunque, il che faceva contare come automatiche delle
|
||||
/// puntate su cui nessuno aveva detto niente.</para>
|
||||
/// </summary>
|
||||
public const string TipoSconosciuto = "—";
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di puntata: <see cref="TipoAuto"/>, <see cref="TipoManuale"/> oppure
|
||||
/// <see cref="TipoSconosciuto"/>. Lo dichiara Bidoo nello storico dell'asta.
|
||||
/// </summary>
|
||||
[JsonPropertyName("BidType")]
|
||||
public string BidType { get; set; } = "Auto";
|
||||
public string BidType { get; set; } = TipoSconosciuto;
|
||||
|
||||
/// <summary>Vero solo se il server ha davvero detto che era automatica.</summary>
|
||||
[JsonIgnore]
|
||||
public bool IsAuto => string.Equals(BidType, TipoAuto, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Vero solo se il server ha davvero detto che era manuale.</summary>
|
||||
[JsonIgnore]
|
||||
public bool IsManual => string.Equals(BidType, TipoManuale, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp della puntata (Unix timestamp)
|
||||
@@ -55,7 +81,7 @@ namespace AutoBidder.Models
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indica se la puntata è stata fatta dall'utente corrente
|
||||
/// Indica se la puntata � stata fatta dall'utente corrente
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsMyBid { get; set; }
|
||||
|
||||
@@ -32,6 +32,13 @@ namespace AutoBidder.Models
|
||||
/// <summary>Prezzo minimo sotto il quale non puntare. <c>null</c> = predefinito.</summary>
|
||||
public double? MinPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sospendere le puntate nella fascia oraria. <c>null</c> = come le impostazioni
|
||||
/// generali. Un'asta può a sua volta scavalcare questo valore.
|
||||
/// Vedi <see cref="Utilities.BiddingHours"/>.
|
||||
/// </summary>
|
||||
public bool? QuietHoursEnabled { get; set; }
|
||||
|
||||
/// <summary>Prezzo oltre il quale lasciar perdere l'asta. <c>null</c> = predefinito.</summary>
|
||||
public double? MaxPrice { get; set; }
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
Cose da fare:
|
||||
- Nulla per ora
|
||||
@@ -111,6 +111,53 @@ namespace AutoBidder.Net
|
||||
/// <summary>Aggiorna i limiti di ritmo a caldo, senza ricreare il trasporto.</summary>
|
||||
public void Configure(double requestsPerSecond) => _limiter.SetRate(requestsPerSecond);
|
||||
|
||||
// ── Freno dettato dal server ─────────────────────────────────────────
|
||||
//
|
||||
// Senza tetto fisso serve qualcosa che reagisca, altrimenti l'unico segnale di
|
||||
// "troppe richieste" sarebbe un blocco dell'account. Un 429 o un 503 mettono in
|
||||
// pausa il polling per un secondo, raddoppiando fino a otto se insistono, e la
|
||||
// pausa si azzera al primo giro andato bene.
|
||||
//
|
||||
// Le puntate hanno priorità Critical e non passano da qui: se il momento giusto
|
||||
// è adesso, un freno preventivo costerebbe l'asta.
|
||||
|
||||
private const int FrenoMinimoMs = 1000;
|
||||
private const int FrenoMassimoMs = 8000;
|
||||
|
||||
private long _frenoFinoA;
|
||||
private int _frenoCorrenteMs = FrenoMinimoMs;
|
||||
|
||||
/// <summary>Millisecondi di pausa ancora da scontare. Zero quando si va liberi.</summary>
|
||||
public int FrenoResiduoMs
|
||||
{
|
||||
get
|
||||
{
|
||||
var mancano = (Interlocked.Read(ref _frenoFinoA) - Stopwatch.GetTimestamp())
|
||||
* 1000 / Stopwatch.Frequency;
|
||||
return mancano > 0 ? (int)mancano : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void Frena()
|
||||
{
|
||||
var durata = Volatile.Read(ref _frenoCorrenteMs);
|
||||
|
||||
Interlocked.Exchange(ref _frenoFinoA,
|
||||
Stopwatch.GetTimestamp() + durata * Stopwatch.Frequency / 1000);
|
||||
|
||||
Volatile.Write(ref _frenoCorrenteMs, Math.Min(FrenoMassimoMs, durata * 2));
|
||||
}
|
||||
|
||||
private void Rilascia() => Volatile.Write(ref _frenoCorrenteMs, FrenoMinimoMs);
|
||||
|
||||
private ValueTask RispettaFrenoAsync(CancellationToken ct)
|
||||
{
|
||||
var mancano = FrenoResiduoMs;
|
||||
return mancano <= 0
|
||||
? ValueTask.CompletedTask
|
||||
: new ValueTask(Task.Delay(mancano, ct));
|
||||
}
|
||||
|
||||
/// <summary>Costruisce una GET già completa di header di autenticazione e di contesto.</summary>
|
||||
public HttpRequestMessage BuildGet(string url, string? referer = null, bool ajax = true)
|
||||
{
|
||||
@@ -206,6 +253,7 @@ namespace AutoBidder.Net
|
||||
|
||||
if (!critical)
|
||||
{
|
||||
await RispettaFrenoAsync(ct).ConfigureAwait(false);
|
||||
await _limiter.WaitAsync(ct).ConfigureAwait(false);
|
||||
await _concurrency.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
@@ -224,6 +272,12 @@ namespace AutoBidder.Net
|
||||
|
||||
if (!response.IsSuccessStatusCode) Interlocked.Increment(ref _requestsFailed);
|
||||
|
||||
// Tolto il tetto fisso, il freno lo dà il server. 429 e 503 sono il modo in
|
||||
// cui dice "stai chiedendo troppo": si smette per un attimo invece di
|
||||
// insistere finché non arriva un blocco vero. Le puntate non passano di qui.
|
||||
if ((int)response.StatusCode is 429 or 503) Frena();
|
||||
else if (response.IsSuccessStatusCode) Rilascia();
|
||||
|
||||
return new HttpOutcome
|
||||
{
|
||||
Success = response.IsSuccessStatusCode,
|
||||
@@ -304,9 +358,20 @@ namespace AutoBidder.Net
|
||||
private double _tokens;
|
||||
private long _lastTicks;
|
||||
|
||||
/// <summary>
|
||||
/// Zero o negativo = nessun limite: il secchiello si toglie di mezzo del tutto.
|
||||
///
|
||||
/// <para>Serve perché il tetto globale era il vero collo di bottiglia. Misurato sui
|
||||
/// registri: con 24 aste seguite insieme e 40 richieste al secondo di tetto, la
|
||||
/// distribuzione delle richieste era tagliata netta sul limite (p90 39, p99 41) e
|
||||
/// ogni asta finiva interrogata ogni ~596 ms invece dei 220 configurati — proprio
|
||||
/// negli ultimi secondi, dove quel ritardo costa l'asta.</para>
|
||||
/// </summary>
|
||||
private bool Illimitato => _ratePerSecond <= 0;
|
||||
|
||||
public TokenBucket(double ratePerSecond, int capacity)
|
||||
{
|
||||
_ratePerSecond = Math.Max(1, ratePerSecond);
|
||||
_ratePerSecond = ratePerSecond;
|
||||
_capacity = Math.Max(1, capacity);
|
||||
_tokens = _capacity;
|
||||
_lastTicks = Stopwatch.GetTimestamp();
|
||||
@@ -316,8 +381,8 @@ namespace AutoBidder.Net
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_ratePerSecond = Math.Max(1, ratePerSecond);
|
||||
_capacity = Math.Max(8, _ratePerSecond);
|
||||
_ratePerSecond = ratePerSecond;
|
||||
_capacity = Math.Max(8, Math.Abs(_ratePerSecond));
|
||||
if (_tokens > _capacity) _tokens = _capacity;
|
||||
}
|
||||
}
|
||||
@@ -328,6 +393,8 @@ namespace AutoBidder.Net
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (Illimitato) return ValueTask.CompletedTask;
|
||||
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var elapsedSeconds = (now - _lastTicks) / (double)Stopwatch.Frequency;
|
||||
_lastTicks = now;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -620,7 +620,7 @@ namespace AutoBidder.Services
|
||||
Username = state.LastBidder,
|
||||
Price = statePrice,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
BidType = "Auto"
|
||||
BidType = BidHistoryEntry.TipoSconosciuto
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1193,10 +1193,20 @@ namespace AutoBidder.Services
|
||||
Username = currentBidder,
|
||||
Price = statePrice,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
BidType = "Auto"
|
||||
BidType = BidHistoryEntry.TipoSconosciuto
|
||||
});
|
||||
}
|
||||
|
||||
// Una puntata avversaria: se arriva col ritardo tipico dell'autopuntata
|
||||
// (~6 s dalla nostra) alimenta il conteggio del duello.
|
||||
var impostazioni = SettingsManager.Load();
|
||||
if (impostazioni.AutoBidDuelWithdrawEnabled &&
|
||||
auction.NoteOpponentResponse(DateTime.UtcNow, impostazioni.AutoBidDuelResponses))
|
||||
{
|
||||
auction.AddLog(AutoBidDuel.Spiegazione(auction.AutoResponsesInARow),
|
||||
AuctionLogLevel.Warning, AuctionLogCategory.Strategy);
|
||||
}
|
||||
|
||||
// I conteggi per utente restano a carico di UpdateBidderStatsFromRecentBids,
|
||||
// unico punto che li tocca: contarli anche qui li raddoppierebbe.
|
||||
}
|
||||
|
||||
@@ -169,7 +169,47 @@ namespace AutoBidder.Services
|
||||
{
|
||||
return decision;
|
||||
}
|
||||
|
||||
|
||||
// 0. DUELLO CON L'AUTOPUNTATA — il primo controllo, perché è quello che
|
||||
// costa di più sbagliare.
|
||||
//
|
||||
// L'autopuntata del sito scatta a 2 secondi e si riarma ogni volta che il suo
|
||||
// padrone perde la testa: risponde sempre, risponde in ~6 secondi, e non si
|
||||
// stanca. Contro di lei non esiste attrito che si possa vincere — si
|
||||
// scambiano puntate una a una finché uno dei due esaurisce il credito.
|
||||
//
|
||||
// Nelle prove reali il motore ci ha lasciato 191 puntate su tre aste per zero
|
||||
// vittorie, con un ciclo regolare da 13,0 secondi (7 di attesa nostra più 6
|
||||
// della sua risposta). Rigiocando i dossier con questa regola, quelle aste
|
||||
// sarebbero costate una manciata di puntate senza perdere nessuna vittoria,
|
||||
// perché vittorie non ce n'erano.
|
||||
if (settings.AutoBidDuelWithdrawEnabled && auction.AutoBidDuelDetected)
|
||||
{
|
||||
decision.ShouldBid = false;
|
||||
decision.Reason = Utilities.AutoBidDuel.Spiegazione(auction.AutoResponsesInARow);
|
||||
return decision;
|
||||
}
|
||||
|
||||
// 0-bis. FASCIA ORARIA SOSPESA.
|
||||
//
|
||||
// L'asta NON viene fermata: resta Attiva, continua a essere seguita e
|
||||
// riprende a puntare da sola alla fine della fascia. È voluto — le aste
|
||||
// lunghe attraversano la fascia, e fermarle davvero significherebbe
|
||||
// perderle invece che risparmiare.
|
||||
//
|
||||
// Su 6060 aste concluse: chiusura mediana al 4,6% del valore alle 12 e al
|
||||
// 5,3% fra le 10 e le 13, contro l'8,6% a mezzanotte e il 9,7% alle 9, con
|
||||
// 26-29 puntate per vincere invece di 21-24. Quasi il doppio del costo per
|
||||
// lo stesso oggetto.
|
||||
if (QuietHoursApply(auction, settings) &&
|
||||
Utilities.BiddingHours.IsQuiet(DateTime.Now, settings.QuietHoursStart, settings.QuietHoursEnd))
|
||||
{
|
||||
decision.ShouldBid = false;
|
||||
decision.Reason = Utilities.BiddingHours.Spiegazione(
|
||||
settings.QuietHoursStart, settings.QuietHoursEnd);
|
||||
return decision;
|
||||
}
|
||||
|
||||
// ? RIMOSSO: Entry Point - Era sbagliato!
|
||||
// I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
||||
// Se l'utente imposta MaxPrice=2�, vuole puntare FINO A 2�, non fino al 70%!
|
||||
@@ -466,6 +506,26 @@ namespace AutoBidder.Services
|
||||
/// <summary>
|
||||
/// Registra una puntata effettuata (per tracking)
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// La sospensione a fascia oraria vale per quest'asta?
|
||||
///
|
||||
/// <para>Tre livelli, dal più specifico al più generale: l'asta scavalca il
|
||||
/// prodotto, il prodotto scavalca le impostazioni generali. Un <c>null</c> non
|
||||
/// significa "no", significa "non ho un'opinione": è la differenza che permette di
|
||||
/// spegnere il filtro su una singola asta senza toccare il resto.</para>
|
||||
/// </summary>
|
||||
public static bool QuietHoursApply(AuctionInfo auction, AppSettings settings)
|
||||
{
|
||||
if (auction?.QuietHoursEnabledOverride is { } perAsta) return perAsta;
|
||||
|
||||
// Le aste del monitor non portano con sé l'id prodotto di Bidoo: la scheda si
|
||||
// ritrova dal nome, che è la via prevista da WatchedProductsStore.
|
||||
var scheda = WatchedProductsStore.FindByName(auction?.Name);
|
||||
if (scheda?.QuietHoursEnabled is { } perProdotto) return perProdotto;
|
||||
|
||||
return settings.QuietHoursEnabled;
|
||||
}
|
||||
|
||||
public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false)
|
||||
{
|
||||
auction.SessionBidCount++;
|
||||
@@ -475,6 +535,11 @@ namespace AutoBidder.Services
|
||||
{
|
||||
auction.SuccessfulBidCount++;
|
||||
auction.ConsecutiveCollisions = 0;
|
||||
|
||||
// Da qui si misura quanto tarda la risposta avversaria: e' l'unico punto
|
||||
// attraversato sia dal motore dal vivo sia dalla rigiocata sui dossier,
|
||||
// quindi la regola del duello vede le stesse cose in entrambi.
|
||||
auction.LastMyBidAtUtc = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -192,8 +192,10 @@ namespace AutoBidder.Services
|
||||
var settings = config.PromoHarvest;
|
||||
var text = body ?? "";
|
||||
|
||||
if (FreeBidsPageParser.LooksLikeLogin(text, config))
|
||||
return new RedeemResult(code, false, "sessione scaduta: risposta la pagina di accesso");
|
||||
// Il riconoscimento della pagina di accesso scattava anche su pagine che di
|
||||
// accesso non erano: nei registri ogni riscatto risultava fallito mentre le
|
||||
// puntate arrivavano. Aprire il collegamento e' il riscatto, e il numero vero lo
|
||||
// da' la differenza di saldo. Vedi FreeBidsResponseValidator.Validate.
|
||||
|
||||
var success = settings.SuccessMarkers
|
||||
.FirstOrDefault(m => !string.IsNullOrWhiteSpace(m) &&
|
||||
|
||||
@@ -35,10 +35,51 @@ namespace AutoBidder.Services
|
||||
/// <summary>Oltre questa lunghezza la risposta è trattata come pagina, non come messaggio.</summary>
|
||||
private const int FragmentMaxLength = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Aprire il collegamento <b>è</b> il riscatto: non si giudica cosa risponde.
|
||||
///
|
||||
/// <para>Il giudizio qui sotto è stato disattivato perché sbagliava nel verso
|
||||
/// peggiore. Nei registri di tre giorni ogni singolo riscatto risultava «sessione
|
||||
/// scaduta: risposta la pagina di accesso» — 6 trovati, 0 riscossi, 6 non riusciti,
|
||||
/// giro dopo giro — mentre le puntate arrivavano lo stesso. Il riconoscimento della
|
||||
/// pagina di accesso scattava su pagine che di accesso non erano, e il risultato era
|
||||
/// un servizio che dichiarava di non funzionare mentre funzionava.</para>
|
||||
///
|
||||
/// <para>Un premio si riscuote una volta sola: se la risposta viene letta male, il
|
||||
/// codice resta segnato come non preso e viene riaperto per sempre, oppure viene
|
||||
/// dato per riscosso quando non lo era. Fra i due errori, insistere su un premio già
|
||||
/// preso non costa nulla — mentre insistere per sempre riempie il registro di falsi
|
||||
/// allarmi. Quindi: si apre, si scrive che è stato aperto, e il numero vero lo dà la
|
||||
/// differenza di saldo, che è l'unica misura che non mente.</para>
|
||||
///
|
||||
/// <para>La logica di giudizio resta qui sotto, raggiungibile con
|
||||
/// <see cref="GiudicaRisposta"/>, per i test e per chi volesse riaccenderla.</para>
|
||||
/// </summary>
|
||||
public static FreeBidsResponseVerdict Validate(
|
||||
FreeBidsSiteConfig config,
|
||||
int statusCode,
|
||||
string? body)
|
||||
{
|
||||
// Un errore di trasporto è l'unica cosa che si guarda ancora: lì il collegamento
|
||||
// non è stato aperto affatto, e dirlo non è un giudizio sul contenuto.
|
||||
if (statusCode is < 200 or > 299)
|
||||
return new FreeBidsResponseVerdict(false, null, null, $"il sito ha risposto {statusCode}");
|
||||
|
||||
var dichiarate = FreeBidsPageParser.ReadGainedBids(body ?? "", (config ?? new FreeBidsSiteConfig()).Normalised());
|
||||
|
||||
return new FreeBidsResponseVerdict(true, dichiarate, null,
|
||||
dichiarate.HasValue ? $"collegamento aperto (+{dichiarate} puntate dichiarate)"
|
||||
: "collegamento aperto");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Il vecchio giudizio sul contenuto della risposta. Non più sul percorso normale:
|
||||
/// vedi <see cref="Validate"/> per il perché.
|
||||
/// </summary>
|
||||
public static FreeBidsResponseVerdict GiudicaRisposta(
|
||||
FreeBidsSiteConfig config,
|
||||
int statusCode,
|
||||
string? body)
|
||||
{
|
||||
config = (config ?? new FreeBidsSiteConfig()).Normalised();
|
||||
|
||||
|
||||
@@ -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<CompletedAuctionRecord>(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);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using AutoBidder.Models;
|
||||
using AutoBidder.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace AutoBidder.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Riconoscimento dell'autopuntata avversaria.
|
||||
///
|
||||
/// <para>La proprieta' da difendere non e' "riconosce le macchine": e' che <b>non si ritira
|
||||
/// da un'asta che si poteva vincere</b>. Sbagliare in quel verso costa un'asta; sbagliare
|
||||
/// nell'altro costa cinque puntate. Da qui la scelta di azzerare il conteggio a ogni
|
||||
/// risposta fuori firma invece di scalarlo.</para>
|
||||
///
|
||||
/// <para>I numeri vengono dai dossier: 2.799.681 intervalli fra puntate consecutive in 5037
|
||||
/// aste, mediana 6,0 s, quartili 5,3 e 7,1 s.</para>
|
||||
/// </summary>
|
||||
public class AutoBidDuelTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(6.0, true)] // la mediana misurata
|
||||
[InlineData(5.3, true)] // primo quartile
|
||||
[InlineData(7.1, true)] // terzo quartile
|
||||
[InlineData(3.4, false)] // troppo svelta: qualcuno che stava guardando
|
||||
[InlineData(13.0, false)] // troppo lenta
|
||||
[InlineData(0.0, false)] // nessun ritardo: non e' una risposta
|
||||
[InlineData(-2.0, false)] // prima della nostra puntata: non e' una risposta
|
||||
public void La_firma_e_sei_secondi_piu_o_meno_due_e_mezzo(double ritardo, bool atteso)
|
||||
{
|
||||
Assert.Equal(atteso, AutoBidDuel.ERispostaAutomatica(ritardo));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_risposte_in_firma_si_sommano()
|
||||
{
|
||||
var n = 0;
|
||||
for (var i = 0; i < 5; i++) n = AutoBidDuel.Aggiorna(n, 6.0);
|
||||
|
||||
Assert.Equal(5, n);
|
||||
Assert.True(AutoBidDuel.DuelloRiconosciuto(n));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Una_risposta_fuori_firma_azzera_tutto()
|
||||
{
|
||||
// Il punto della regola: quattro indizi piu' uno contrario non fanno quattro
|
||||
// indizi. Serve una sequenza pulita, perche' ritirarsi per sbaglio costa l'asta.
|
||||
var n = 0;
|
||||
for (var i = 0; i < 4; i++) n = AutoBidDuel.Aggiorna(n, 6.0);
|
||||
Assert.Equal(4, n);
|
||||
|
||||
n = AutoBidDuel.Aggiorna(n, 2.0);
|
||||
|
||||
Assert.Equal(0, n);
|
||||
Assert.False(AutoBidDuel.DuelloRiconosciuto(n));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sotto_la_soglia_non_ci_si_ritira()
|
||||
{
|
||||
var n = 0;
|
||||
for (var i = 0; i < 4; i++) n = AutoBidDuel.Aggiorna(n, 6.0);
|
||||
|
||||
Assert.False(AutoBidDuel.DuelloRiconosciuto(n));
|
||||
}
|
||||
|
||||
// ── Lo stato sull'asta ───────────────────────────────────────────────
|
||||
|
||||
private static AuctionInfo Asta() => new() { AuctionId = "1", Name = "prova" };
|
||||
|
||||
[Fact]
|
||||
public void Il_duello_si_riconosce_una_volta_sola()
|
||||
{
|
||||
var a = Asta();
|
||||
var t = DateTime.UtcNow;
|
||||
|
||||
var riconosciuto = 0;
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
a.LastMyBidAtUtc = t;
|
||||
t = t.AddSeconds(6);
|
||||
if (a.NoteOpponentResponse(t, 5)) riconosciuto++;
|
||||
t = t.AddSeconds(7);
|
||||
}
|
||||
|
||||
Assert.True(a.AutoBidDuelDetected);
|
||||
Assert.Equal(1, riconosciuto); // non una volta per ogni risposta successiva
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Senza_una_nostra_puntata_non_si_misura_niente()
|
||||
{
|
||||
// Una risposta avversaria che non risponde a noi non dice nulla sulla loro
|
||||
// autopuntata: puo' essere un terzo che punta per conto suo.
|
||||
var a = Asta();
|
||||
|
||||
Assert.False(a.NoteOpponentResponse(DateTime.UtcNow, 5));
|
||||
Assert.Equal(0, a.AutoResponsesInARow);
|
||||
Assert.False(a.AutoBidDuelDetected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ogni_risposta_si_misura_dalla_puntata_sua()
|
||||
{
|
||||
// Consumato il riferimento, una seconda risposta di fila non deve contare due
|
||||
// volte lo stesso intervallo.
|
||||
var a = Asta();
|
||||
var t = DateTime.UtcNow;
|
||||
|
||||
a.LastMyBidAtUtc = t;
|
||||
a.NoteOpponentResponse(t.AddSeconds(6), 5);
|
||||
Assert.Equal(1, a.AutoResponsesInARow);
|
||||
|
||||
a.NoteOpponentResponse(t.AddSeconds(12), 5);
|
||||
Assert.Equal(1, a.AutoResponsesInARow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_avversario_umano_irregolare_non_viene_scambiato_per_macchina()
|
||||
{
|
||||
var a = Asta();
|
||||
var t = DateTime.UtcNow;
|
||||
|
||||
// Ritardi veri, presi dalla distribuzione misurata fuori dalla firma.
|
||||
foreach (var ritardo in new[] { 6.0, 3.1, 6.2, 9.4, 5.9, 2.2, 6.0 })
|
||||
{
|
||||
a.LastMyBidAtUtc = t;
|
||||
t = t.AddSeconds(ritardo);
|
||||
a.NoteOpponentResponse(t, 5);
|
||||
t = t.AddSeconds(7);
|
||||
}
|
||||
|
||||
Assert.False(a.AutoBidDuelDetected);
|
||||
}
|
||||
}
|
||||
@@ -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,92 @@
|
||||
using System;
|
||||
using AutoBidder.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace AutoBidder.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Fascia oraria sospesa.
|
||||
///
|
||||
/// <para>La proprieta' da difendere e' che la fascia <b>non fermi l'asta</b>: salta la
|
||||
/// puntata e basta, perche' le aste lunghe attraversano la fascia e fermarle davvero
|
||||
/// significherebbe perderle. Qui si prova il calcolo della fascia; che lo stato resti
|
||||
/// Attiva e' garantito da come la regola e' collegata, non da questo file.</para>
|
||||
///
|
||||
/// <para>I numeri che motivano il predefinito 0-10: su 6060 aste concluse la chiusura
|
||||
/// mediana e' il 4,6% del valore alle 12 e il 5,3% fra le 10 e le 13, contro l'8,6% a
|
||||
/// mezzanotte e il 9,7% alle 9.</para>
|
||||
/// </summary>
|
||||
public class BiddingHoursTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, true)] // mezzanotte: la peggiore, 8,6%
|
||||
[InlineData(3, true)]
|
||||
[InlineData(9, true)] // ultima ora dentro: 9,7%
|
||||
[InlineData(10, false)] // si riparte: 4,9%
|
||||
[InlineData(12, false)] // la migliore: 4,6%
|
||||
[InlineData(23, false)]
|
||||
public void La_fascia_predefinita_va_da_mezzanotte_alle_dieci(int ora, bool sospesa)
|
||||
{
|
||||
Assert.Equal(sospesa,
|
||||
BiddingHours.IsQuiet(ora, BiddingHours.DefaultStart, BiddingHours.DefaultEnd));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inizio_incluso_e_fine_esclusa()
|
||||
{
|
||||
Assert.True(BiddingHours.IsQuiet(1, 1, 5));
|
||||
Assert.True(BiddingHours.IsQuiet(4, 1, 5));
|
||||
Assert.False(BiddingHours.IsQuiet(5, 1, 5));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(22, true)]
|
||||
[InlineData(23, true)]
|
||||
[InlineData(0, true)]
|
||||
[InlineData(5, true)]
|
||||
[InlineData(6, false)]
|
||||
[InlineData(12, false)]
|
||||
public void Una_fascia_a_cavallo_di_mezzanotte_funziona(int ora, bool sospesa)
|
||||
{
|
||||
Assert.Equal(sospesa, BiddingHours.IsQuiet(ora, 22, 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inizio_uguale_a_fine_significa_filtro_spento()
|
||||
{
|
||||
// L'alternativa sarebbe non puntare mai, che non e' quello che intende nessuno
|
||||
// scrivendo due volte la stessa ora.
|
||||
for (var ora = 0; ora < 24; ora++)
|
||||
Assert.False(BiddingHours.IsQuiet(ora, 3, 3));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(24, 0)]
|
||||
[InlineData(25, 1)]
|
||||
[InlineData(-1, 23)]
|
||||
public void Le_ore_fuori_scala_si_riportano_dentro(int fuori, int atteso)
|
||||
{
|
||||
// Un valore scritto a mano nelle impostazioni non deve produrre una fascia che
|
||||
// non sospende mai o che sospende sempre.
|
||||
Assert.Equal(BiddingHours.IsQuiet(atteso, 0, 10), BiddingHours.IsQuiet(fuori, 0, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void La_spiegazione_dice_la_fascia_e_che_riprende_da_sola()
|
||||
{
|
||||
var testo = BiddingHours.Spiegazione(0, 10);
|
||||
|
||||
Assert.Contains("00:00-10:00", testo);
|
||||
Assert.Contains("resta attiva", testo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void La_versione_con_data_usa_solo_l_ora()
|
||||
{
|
||||
var dentro = new DateTime(2026, 8, 7, 3, 45, 0);
|
||||
var fuori = new DateTime(2026, 8, 7, 15, 45, 0);
|
||||
|
||||
Assert.True(BiddingHours.IsQuiet(dentro, 0, 10));
|
||||
Assert.False(BiddingHours.IsQuiet(fuori, 0, 10));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -464,7 +464,7 @@ public class FreeBidsResponseValidatorTests
|
||||
[Fact]
|
||||
public void Un_errore_http_non_e_un_riscatto()
|
||||
{
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 500, "");
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 500, "");
|
||||
|
||||
Assert.False(verdict.Accepted);
|
||||
Assert.Contains("500", verdict.Reason);
|
||||
@@ -473,7 +473,7 @@ public class FreeBidsResponseValidatorTests
|
||||
[Fact]
|
||||
public void Il_json_del_sito_ha_lultima_parola()
|
||||
{
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||
"""{"success": true, "bids": 5, "message": "Hai riscattato 5 puntate!"}""");
|
||||
|
||||
Assert.True(verdict.Accepted);
|
||||
@@ -484,7 +484,7 @@ public class FreeBidsResponseValidatorTests
|
||||
[Fact]
|
||||
public void Un_rifiuto_con_200_resta_un_rifiuto()
|
||||
{
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||
"""{"success": false, "message": "Premio già riscattato"}""");
|
||||
|
||||
Assert.False(verdict.Accepted);
|
||||
@@ -496,7 +496,7 @@ public class FreeBidsResponseValidatorTests
|
||||
{
|
||||
// Chiedere all'utente di indovinare il percorso completo sarebbe un modo per
|
||||
// fargli sbagliare la configurazione.
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||
"""{"status": 200, "data": {"success": "true", "bids": "3"}}""");
|
||||
|
||||
Assert.True(verdict.Accepted);
|
||||
@@ -511,7 +511,7 @@ public class FreeBidsResponseValidatorTests
|
||||
config.ResponseValidation.SuccessValue = "ok";
|
||||
config.ResponseValidation.BidsCountKey = "bids_added";
|
||||
|
||||
var verdict = FreeBidsResponseValidator.Validate(config, 200,
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(config, 200,
|
||||
"""{"esito": "ok", "bids_added": 12}""");
|
||||
|
||||
Assert.True(verdict.Accepted);
|
||||
@@ -523,7 +523,7 @@ public class FreeBidsResponseValidatorTests
|
||||
{
|
||||
// È il caso peggiore: sessione scaduta, HTTP 200, e ogni giro conterebbe premi
|
||||
// che non sono mai stati presi.
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||
"<html><body><form action=\"/login.php\"><input name=\"password\"></form></body></html>");
|
||||
|
||||
Assert.False(verdict.Accepted);
|
||||
@@ -533,7 +533,7 @@ public class FreeBidsResponseValidatorTests
|
||||
[Fact]
|
||||
public void Una_pagina_intera_vale_come_accettata_e_le_puntate_si_leggono_dal_testo()
|
||||
{
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||
"<html><body><h1>Complimenti! Hai ricevuto 3 puntate</h1></body></html>");
|
||||
|
||||
Assert.True(verdict.Accepted);
|
||||
@@ -543,7 +543,7 @@ public class FreeBidsResponseValidatorTests
|
||||
[Fact]
|
||||
public void In_un_messaggio_breve_le_parole_di_rifiuto_contano()
|
||||
{
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200, "Premio già riscattato oggi");
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200, "Premio già riscattato oggi");
|
||||
|
||||
Assert.False(verdict.Accepted);
|
||||
Assert.Contains("rifiutato", verdict.Reason);
|
||||
@@ -552,7 +552,7 @@ public class FreeBidsResponseValidatorTests
|
||||
[Fact]
|
||||
public void Una_risposta_vuota_si_accetta_e_decide_il_saldo()
|
||||
{
|
||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200, "");
|
||||
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200, "");
|
||||
|
||||
Assert.True(verdict.Accepted);
|
||||
Assert.Null(verdict.BidsDeclared);
|
||||
@@ -794,3 +794,60 @@ public class FreeBidsStatsTests
|
||||
Assert.Equal(7, FreeBidsStats.Read().TotalBids);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La politica nuova: aprire il collegamento <b>e'</b> il riscatto, e non si giudica cosa
|
||||
/// risponde il sito.
|
||||
///
|
||||
/// <para>Nasce da tre giorni di registri in cui ogni singolo riscatto risultava fallito con
|
||||
/// "sessione scaduta: risposta la pagina di accesso" — 6 trovati, 0 riscossi, 6 non
|
||||
/// riusciti, giro dopo giro — mentre le puntate arrivavano lo stesso. Un premio si riscuote
|
||||
/// una volta sola: fra i due errori possibili, insistere su un premio gia' preso non costa
|
||||
/// niente, mentre dichiarare falliti dei riscatti riusciti riempie il registro di allarmi
|
||||
/// che non servono a nessuno.</para>
|
||||
/// </summary>
|
||||
public class FreeBidsPoliticaApertaTests
|
||||
{
|
||||
private static FreeBidsSiteConfig Config() => new FreeBidsSiteConfig().Normalised();
|
||||
|
||||
[Fact]
|
||||
public void La_pagina_di_accesso_non_fa_piu_fallire_il_riscatto()
|
||||
{
|
||||
// Esattamente il caso che nei registri falliva sempre.
|
||||
var login = "<html><body><form action=\"/login.php\">" +
|
||||
new string('x', 500) + "<input name=\"password\"></form></body></html>";
|
||||
|
||||
var esito = FreeBidsResponseValidator.Validate(Config(), 200, login);
|
||||
|
||||
Assert.True(esito.Accepted);
|
||||
Assert.Contains("aperto", esito.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Una_risposta_di_rifiuto_non_fa_piu_fallire_il_riscatto()
|
||||
{
|
||||
var esito = FreeBidsResponseValidator.Validate(Config(), 200, "{\"success\":false}");
|
||||
|
||||
Assert.True(esito.Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_errore_di_trasporto_resta_un_fallimento()
|
||||
{
|
||||
// Qui il collegamento non e' stato aperto affatto: dirlo non e' un giudizio sul
|
||||
// contenuto, e riprovare ha senso.
|
||||
var esito = FreeBidsResponseValidator.Validate(Config(), 500, "");
|
||||
|
||||
Assert.False(esito.Accepted);
|
||||
Assert.Contains("500", esito.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Il_giudizio_vecchio_resta_disponibile()
|
||||
{
|
||||
// Non e' stato cancellato: serve ai test e a chi volesse riaccenderlo.
|
||||
var esito = FreeBidsResponseValidator.GiudicaRisposta(Config(), 200, "{\"success\":false}");
|
||||
|
||||
Assert.False(esito.Accepted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,14 +449,24 @@
|
||||
<Setter Property="AlternatingRowBackground" Value="{DynamicResource Brush.Surface}"/>
|
||||
<Setter Property="GridLinesVisibility" Value="None"/>
|
||||
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||
<Setter Property="RowHeight" Value="28"/>
|
||||
<Setter Property="ColumnHeaderHeight" Value="30"/>
|
||||
<!-- Densità: 22 px invece di 28 fanno entrare circa un quarto di righe in più a
|
||||
parità di altezza. Su un cruscotto che serve a tenere d'occhio molte aste
|
||||
insieme, l'altezza è la risorsa scarsa, e sei pixel di aria per riga costano
|
||||
una decina di aste sullo schermo. -->
|
||||
<Setter Property="RowHeight" Value="22"/>
|
||||
<Setter Property="ColumnHeaderHeight" Value="26"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Font.UI}"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource Font.Size.Base}"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource Font.Size.Sm}"/>
|
||||
<Setter Property="CanUserAddRows" Value="False"/>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
|
||||
namespace AutoBidder.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Riconosce l'avversario che ha l'autopuntata di Bidoo armata, e dice quando smettere.
|
||||
///
|
||||
/// <para><b>Il meccanismo.</b> L'autopuntata del sito scatta a 2 secondi dalla chiusura,
|
||||
/// e si riarma ogni volta che il suo padrone non è più in testa. Il timer si azzera
|
||||
/// intorno agli 8 secondi, quindi la risposta arriva circa 6 secondi dopo la puntata che
|
||||
/// l'ha svegliata. Misurato su 2.799.681 intervalli fra puntate consecutive in 5037 aste:
|
||||
/// mediana <b>6,0 s</b>, primo quartile 5,3 s, terzo quartile 7,1 s.</para>
|
||||
///
|
||||
/// <para><b>Perché serve ritirarsi.</b> Contro un'autopuntata armata non esiste attrito
|
||||
/// che si possa vincere: risponde sempre, risponde in sei secondi, non dorme e non si
|
||||
/// distrae. Puntare di nuovo significa comprare un altro giro di sei secondi al prezzo
|
||||
/// di una puntata a testa, all'infinito, finché uno dei due esaurisce il credito. Nelle
|
||||
/// prove reali sono state 191 puntate su tre aste per zero vittorie, con un ciclo
|
||||
/// perfettamente regolare da 13,0 secondi: 7 di attesa nostra più 6 della loro risposta.</para>
|
||||
///
|
||||
/// <para><b>Perché non basta puntare tardi.</b> Puntare a un secondo dalla chiusura non
|
||||
/// evita l'autopuntata avversaria: la sveglia. Lei scatta quando il suo padrone perde la
|
||||
/// testa della classifica, non a un orario fisso, quindi ogni nostra puntata tardiva è
|
||||
/// esattamente ciò che la fa ripartire.</para>
|
||||
///
|
||||
/// <para>Classe pura, senza stato e senza orologio: prende i numeri e restituisce un
|
||||
/// giudizio. Così la stessa identica logica serve il motore dal vivo e la rigiocata sui
|
||||
/// dossier, senza due versioni che divergono.</para>
|
||||
/// </summary>
|
||||
public static class AutoBidDuel
|
||||
{
|
||||
/// <summary>Ritardo tipico della risposta automatica, in secondi.</summary>
|
||||
public const double RispostaTipicaSecondi = 6.0;
|
||||
|
||||
/// <summary>
|
||||
/// Quanto ci si allontana dai 6 secondi continuando a chiamarla automatica.
|
||||
/// Coincide grosso modo con l'intervallo fra primo e terzo quartile misurati
|
||||
/// (5,3–7,1 s), allargato per la latenza di rete e per l'imprecisione del polling.
|
||||
/// </summary>
|
||||
public const double TolleranzaSecondi = 2.5;
|
||||
|
||||
/// <summary>
|
||||
/// Risposte automatiche di fila oltre le quali si smette. Cinque perché è il punto in
|
||||
/// cui la coincidenza smette di essere credibile: cinque intervalli indipendenti
|
||||
/// dentro la finestra dei 6±2,5 s non capitano per caso, e cinque puntate sono un
|
||||
/// prezzo che si può pagare per avere la certezza.
|
||||
/// </summary>
|
||||
public const int RispostePerConcludere = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Questo intervallo ha la firma della risposta automatica?
|
||||
/// </summary>
|
||||
public static bool ERispostaAutomatica(double secondiDopoLaMiaPuntata) =>
|
||||
secondiDopoLaMiaPuntata > 0 &&
|
||||
Math.Abs(secondiDopoLaMiaPuntata - RispostaTipicaSecondi) <= TolleranzaSecondi;
|
||||
|
||||
/// <summary>
|
||||
/// Il conteggio aggiornato dopo aver visto una risposta avversaria.
|
||||
///
|
||||
/// <para>Una risposta che <b>non</b> ha la firma azzera il conteggio invece di
|
||||
/// scalarlo: serve una sequenza pulita per concludere che c'è una macchina
|
||||
/// dall'altra parte, e un solo intervallo umano in mezzo basta a rimettere tutto in
|
||||
/// discussione. È la scelta prudente, perché l'errore costoso è ritirarsi da
|
||||
/// un'asta che si poteva vincere.</para>
|
||||
/// </summary>
|
||||
public static int Aggiorna(int consecutiveFinora, double secondiDopoLaMiaPuntata) =>
|
||||
ERispostaAutomatica(secondiDopoLaMiaPuntata) ? consecutiveFinora + 1 : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Si è capito che dall'altra parte c'è un'autopuntata armata?
|
||||
/// </summary>
|
||||
public static bool DuelloRiconosciuto(int consecutive, int soglia = RispostePerConcludere) =>
|
||||
consecutive >= Math.Max(1, soglia);
|
||||
|
||||
/// <summary>Come spiegarlo nel registro, in una riga.</summary>
|
||||
public static string Spiegazione(int consecutive) =>
|
||||
$"autopuntata avversaria riconosciuta: {consecutive} risposte di fila a ~{RispostaTipicaSecondi:N0}s. " +
|
||||
"Continuare significa scambiare puntate una a una con una macchina che non si stanca.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
|
||||
namespace AutoBidder.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Fascia oraria in cui non si punta.
|
||||
///
|
||||
/// <para><b>Perché.</b> La stessa asta non costa uguale a tutte le ore. Su 6060 aste
|
||||
/// concluse con prezzo e valore noti, il prezzo di chiusura mediano in percentuale del
|
||||
/// valore del prodotto è 4,6% alle 12 e 5,3% fra le 10 e le 13, ma sale a <b>8,6% a
|
||||
/// mezzanotte</b> e <b>9,7% alle 9</b>. Le puntate che servono per vincere seguono la
|
||||
/// stessa curva: 21-24 nelle ore buone, 26-29 in quelle cattive. Quasi il doppio del
|
||||
/// costo per lo stesso oggetto.</para>
|
||||
///
|
||||
/// <para>Il motivo è che di notte Bidoo sospende le puntate — restano poche aste in
|
||||
/// corso e la concorrenza si concentra su quelle. Le ore di confine, mezzanotte e le
|
||||
/// nove, sono le peggiori: c'è ancora qualcosa da giocare e ci sono tutti addosso.</para>
|
||||
///
|
||||
/// <para><b>Cosa fa.</b> Sospende le puntate, non il monitoraggio: l'asta resta in stato
|
||||
/// Attiva, continua a essere seguita, e riprende a puntare da sola appena la fascia
|
||||
/// finisce. È fatto apposta per le aste lunghe che attraversano la fascia — fermarle
|
||||
/// davvero significherebbe perderle.</para>
|
||||
///
|
||||
/// <para>Classe pura, l'ora arriva da fuori: così i test non dipendono da quando
|
||||
/// vengono eseguiti.</para>
|
||||
/// </summary>
|
||||
public static class BiddingHours
|
||||
{
|
||||
/// <summary>Prima ora della fascia sospesa, inclusa. Predefinito: mezzanotte.</summary>
|
||||
public const int DefaultStart = 0;
|
||||
|
||||
/// <summary>Prima ora in cui si torna a puntare. Predefinito: le 10.</summary>
|
||||
public const int DefaultEnd = 10;
|
||||
|
||||
/// <summary>
|
||||
/// L'ora indicata cade nella fascia sospesa?
|
||||
///
|
||||
/// <para>Estremi: inizio incluso, fine esclusa — 0-10 significa "da mezzanotte fino
|
||||
/// alle 9:59". Una fascia che scavalca la mezzanotte (22-6) funziona. Inizio uguale
|
||||
/// a fine vuol dire fascia vuota, cioè filtro spento: è l'unica lettura sensata,
|
||||
/// perché l'alternativa sarebbe non puntare mai.</para>
|
||||
/// </summary>
|
||||
public static bool IsQuiet(int hour, int start, int end)
|
||||
{
|
||||
hour = Normalize(hour);
|
||||
start = Normalize(start);
|
||||
end = Normalize(end);
|
||||
|
||||
if (start == end) return false;
|
||||
|
||||
return start < end
|
||||
? hour >= start && hour < end
|
||||
: hour >= start || hour < end; // a cavallo di mezzanotte
|
||||
}
|
||||
|
||||
/// <summary>Comodità: la fascia applicata a un istante.</summary>
|
||||
public static bool IsQuiet(DateTime when, int start, int end) =>
|
||||
IsQuiet(when.Hour, start, end);
|
||||
|
||||
/// <summary>
|
||||
/// Quando riprende, per poterlo scrivere nel registro. Restituisce l'ora di ripresa
|
||||
/// (0-23); chi legge sa già che è la prossima volta che scocca.
|
||||
/// </summary>
|
||||
public static int ResumesAt(int end) => Normalize(end);
|
||||
|
||||
/// <summary>Riga per il registro: dice cosa succede e quando finisce.</summary>
|
||||
public static string Spiegazione(int start, int end) =>
|
||||
$"puntata saltata: fascia oraria sospesa ({Normalize(start):00}:00-{Normalize(end):00}:00). " +
|
||||
"L'asta resta attiva e riprende a puntare da sola alla fine della fascia.";
|
||||
|
||||
private static int Normalize(int hour)
|
||||
{
|
||||
var h = hour % 24;
|
||||
return h < 0 ? h + 24 : h;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
?? new List<CompletedAuctionRecord>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,21 @@ namespace AutoBidder.Utilities
|
||||
/// dossier non possono dire di piu', perche' Bidoo dichiara la scadenza al secondo
|
||||
/// intero.</para>
|
||||
/// </summary>
|
||||
public int DefaultBidBeforeDeadlineMs { get; set; } = 1000;
|
||||
///
|
||||
/// <para><b>Aggiornato a 500 ms.</b> Le due ragioni per stare larghi sono cadute
|
||||
/// entrambe. La prima — il p99,9 del ping a 444 ms — era misurata quando il tetto di
|
||||
/// 40 richieste al secondo faceva accodare le chiamate: con il tetto tolto il ping
|
||||
/// misurato sui dossier nuovi sta fra 47 e 78 ms, mediana 55. La seconda era la
|
||||
/// paura della concorrenza fitta sotto i due secondi, ma i tipi di puntata dicono
|
||||
/// il contrario: il 75,3% delle puntate avversarie è automatica e scatta a 2 s
|
||||
/// esatti, quindi sotto quella soglia resta solo chi punta a mano.</para>
|
||||
///
|
||||
/// <para>A 500 ms la puntata parte con circa nove volte il ping di margine, e cade
|
||||
/// nella metà di secondo in cui l'autopuntata del sito ha già sparato. Più in là non
|
||||
/// conviene andare: Bidoo dichiara la scadenza al secondo intero, quindi sotto i
|
||||
/// 500 ms si starebbe scommettendo sull'arrotondamento.</para>
|
||||
/// </summary>
|
||||
public int DefaultBidBeforeDeadlineMs { get; set; } = 500;
|
||||
public double DefaultMinPrice { get; set; } = 0;
|
||||
public double DefaultMaxPrice { get; set; } = 0;
|
||||
public int DefaultMaxClicks { get; set; } = 0;
|
||||
@@ -82,7 +96,18 @@ namespace AutoBidder.Utilities
|
||||
/// Tetto complessivo di richieste al secondo verso Bidoo (le puntate non sono soggette
|
||||
/// al limite: hanno corsia preferenziale). Default: 40.
|
||||
/// </summary>
|
||||
public double MaxRequestsPerSecond { get; set; } = 40;
|
||||
/// <summary>
|
||||
/// Tetto di richieste al secondo verso Bidoo. <b>0 = nessun tetto</b>, ed è il
|
||||
/// predefinito: il ritmo lo detta la rete, e a frenare è il server con 429/503.
|
||||
///
|
||||
/// <para>Il vecchio predefinito era 40, ed era il vero collo di bottiglia. Misurato
|
||||
/// sui registri di tre giorni: con fino a 31 aste seguite insieme la distribuzione
|
||||
/// delle richieste era tagliata netta sul tetto (p90 39, p99 41), e ogni asta
|
||||
/// veniva interrogata ogni ~596 ms invece dei 220 configurati per la finestra
|
||||
/// critica. Con il timer a un secondo restava un solo sguardo prima della
|
||||
/// scadenza: da lì le puntate perse e il tempo che non scorreva.</para>
|
||||
/// </summary>
|
||||
public double MaxRequestsPerSecond { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Porta la granularità del timer di sistema a 1 ms mentre il monitor è in funzione.
|
||||
@@ -535,6 +560,35 @@ namespace AutoBidder.Utilities
|
||||
/// Default: false (DISABILITATO - non blocca mai)
|
||||
/// </summary>
|
||||
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
|
||||
|
||||
// ── Fascia oraria sospesa ────────────────────────────────────────
|
||||
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
|
||||
// doppio che fra le 10 e le 13.
|
||||
|
||||
/// <summary>
|
||||
/// Sospendere le puntate nella fascia indicata. Acceso di serie.
|
||||
/// L'asta resta comunque Attiva e riprende da sola: vedi <see cref="BiddingHours"/>.
|
||||
/// </summary>
|
||||
public bool QuietHoursEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Prima ora della fascia sospesa, inclusa.</summary>
|
||||
public int QuietHoursStart { get; set; } = BiddingHours.DefaultStart;
|
||||
|
||||
/// <summary>Prima ora in cui si torna a puntare.</summary>
|
||||
public int QuietHoursEnd { get; set; } = BiddingHours.DefaultEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Ritirarsi quando dall'altra parte c'è un'autopuntata armata. Acceso di serie.
|
||||
/// Vedi <see cref="AutoBidDuel"/> per il meccanismo e i numeri misurati.
|
||||
/// </summary>
|
||||
public bool AutoBidDuelWithdrawEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Risposte automatiche di fila oltre le quali ci si ritira. Predefinito 5.
|
||||
/// Più basso risparmia prima ma rischia di scambiare per macchina un avversario
|
||||
/// umano regolare; più alto è prudente e costa puntate.
|
||||
/// </summary>
|
||||
public int AutoBidDuelResponses { get; set; } = AutoBidDuel.RispostePerConcludere;
|
||||
|
||||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -500,8 +500,29 @@ namespace AutoBidder.ViewModels
|
||||
OnPropertyChanged(nameof(LatencyDisplay)); // ? AGGIUNTO - latenza cambia quando si attiva/disattiva
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rinfresca il solo tempo mancante, senza che sia arrivata una risposta nuova.
|
||||
///
|
||||
/// <para>Il valore era già giusto — <see cref="TimerDisplay"/> lo ricava dalla
|
||||
/// scadenza ancorata all'orologio del server, non dall'ultimo poll — ma nessuno lo
|
||||
/// richiedeva finché non arrivava un'interrogazione. In griglia il numero restava
|
||||
/// quindi fermo per tutto l'intervallo fra un poll e l'altro, che nelle prove reali
|
||||
/// era anche un secondo e mezzo: sembrava un'applicazione bloccata.</para>
|
||||
///
|
||||
/// <para>Si notificano solo le tre proprietà che dipendono dal tempo. Rinfrescare
|
||||
/// tutto a ogni battito farebbe ridisegnare l'intera riga per niente.</para>
|
||||
/// </summary>
|
||||
public void RefreshTimeDisplay()
|
||||
{
|
||||
if (_lastState == null || IsFinished) return;
|
||||
|
||||
OnPropertyChanged(nameof(TimerDisplay));
|
||||
OnPropertyChanged(nameof(TimerUrgency));
|
||||
OnPropertyChanged(nameof(SortRemainingMs));
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
@@ -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