Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b9691d692 | ||
|
|
5bac29b0af | ||
|
|
9b94e2d405 | ||
|
|
20434ca273 | ||
|
|
feb39ee2de |
@@ -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"
|
||||
@@ -526,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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,8 +0,0 @@
|
||||
Cose da fare:
|
||||
- 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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,10 +449,14 @@
|
||||
<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"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user