Compare commits
| 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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
ToolTip="Ferma tutte: nessuna richiesta"
|
ToolTip="Ferma tutte: nessuna richiesta"
|
||||||
Click="StopButton_Click">
|
Click="StopButton_Click">
|
||||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
@@ -390,6 +391,7 @@
|
|||||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Style="{StaticResource MiniIconButton}"
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
ToolTip="Ferma"
|
ToolTip="Ferma"
|
||||||
IsEnabled="{Binding CanStop}"
|
IsEnabled="{Binding CanStop}"
|
||||||
Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
@@ -524,14 +526,18 @@
|
|||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo puntata (ms)"
|
<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.
|
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}"/>
|
Style="{StaticResource FieldLabel}"/>
|
||||||
<TextBox Grid.Row="1" Grid.Column="0" x:Name="SelectedBidBeforeDeadlineMs"
|
<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"
|
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.
|
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 €"
|
<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."
|
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 (€)"
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Costo medio di una puntata (€)"
|
||||||
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo."/>
|
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo. Il valore predefinito è 0,20 €, cioè il pacchetto meno vantaggioso in vendita su Bidoo: tenerlo così fa stimare per eccesso, e una stima per eccesso al massimo ti fa rinunciare a un affare, mentre una per difetto ti fa spendere più del valore del prodotto. Se compri pacchetti più grandi, abbassalo al costo che paghi davvero."/>
|
||||||
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
||||||
Text="0,20" Margin="10,10"/>
|
Text="0,20" Margin="10,10"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace AutoBidder
|
|||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private DispatcherTimer? _headerTimer;
|
private DispatcherTimer? _headerTimer;
|
||||||
|
private DispatcherTimer? _timerTicker;
|
||||||
|
|
||||||
private void StartMonitorHeaderTimer()
|
private void StartMonitorHeaderTimer()
|
||||||
{
|
{
|
||||||
@@ -25,9 +26,35 @@ namespace AutoBidder
|
|||||||
_headerTimer.Tick += (_, _) => RefreshMonitorHeader();
|
_headerTimer.Tick += (_, _) => RefreshMonitorHeader();
|
||||||
_headerTimer.Start();
|
_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();
|
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()
|
private void RefreshMonitorHeader()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -46,6 +46,17 @@
|
|||||||
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
||||||
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
|
||||||
|
Content="Aste seguite per meno del"
|
||||||
|
ToolTip="Toglie le aste agganciate a metà. Sono il caso peggiore: i numeri sono veri ma parziali, quindi sembrano buone e abbassano le medie senza dare segnali. Quanta asta si è vista si ricava dal prezzo finale, senza bisogno di alcun dato in più: ogni puntata vale un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta, e i reset osservati dicono quanti se ne sono visti. Sul tuo storico, su 5876 aste: al 75% ne resterebbero circa 78 su 100, al 90% circa 64, al 95% circa 54, al 99% circa 34."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
<TextBox x:Name="CoverageBox" Text="90" Width="50" Margin="8,0"
|
||||||
|
VerticalAlignment="Center" TextChanged="Option_Changed"/>
|
||||||
|
<TextBlock Text="% della loro durata" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
||||||
Content="Aste chiuse da più di"
|
Content="Aste chiuse da più di"
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ namespace AutoBidder.Dialogs
|
|||||||
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Una soglia illeggibile o fuori scala non deve diventare "togli tutto":
|
||||||
|
// il filtro resta spento finché il numero non ha senso.
|
||||||
|
if (ChkCoverage.IsChecked == true &&
|
||||||
|
double.TryParse(CoverageBox.Text.Trim().Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var percento) &&
|
||||||
|
percento is > 0 and <= 100)
|
||||||
|
{
|
||||||
|
options.MinObservedCoverage = percento / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
if (ChkOlderThan.IsChecked == true &&
|
if (ChkOlderThan.IsChecked == true &&
|
||||||
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -164,22 +164,28 @@ namespace AutoBidder.Engine
|
|||||||
private int NextPollDelay(AppSettings settings)
|
private int NextPollDelay(AppSettings settings)
|
||||||
{
|
{
|
||||||
// Un'asta non ancora cominciata ha una cadenza tutta sua: vedi NotStartedDelay.
|
// 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);
|
var notStarted = NotStartedDelay(settings);
|
||||||
if (notStarted > 0) return notStarted;
|
if (notStarted > 0) return notStarted;
|
||||||
|
|
||||||
var remaining = _auction.EstimatedRemainingMs();
|
// ── Nessuna attesa: la cadenza la detta la rete ──────────────────────
|
||||||
|
//
|
||||||
if (remaining == double.MaxValue) return settings.PollIntervalFarMs;
|
// Prima qui c'erano quattro soglie configurabili (lontano / medio / vicino /
|
||||||
|
// critico). Sui registri di tre giorni non hanno mai funzionato come scritto:
|
||||||
// In sola osservazione non c'è puntata da azzeccare: si risparmiano chiamate.
|
// con 40 richieste al secondo di tetto globale spartite fra tutte le aste
|
||||||
var critical = _auction.State == RunState.Active
|
// seguite, i 220 ms della finestra critica diventavano 596 ms misurati, e con
|
||||||
? settings.PollIntervalCriticalMs
|
// il timer a un secondo restava un solo sguardo prima della scadenza.
|
||||||
: settings.PollIntervalNearMs;
|
//
|
||||||
|
// Il ritmo adesso è quello del giro di andata e ritorno: si riparte appena la
|
||||||
if (remaining <= settings.CriticalWindowMs) return critical;
|
// risposta è arrivata. Con ping ~57 ms misurato sono circa diciassette
|
||||||
if (remaining <= 10_000) return settings.PollIntervalNearMs;
|
// interrogazioni al secondo per asta.
|
||||||
if (remaining <= 60_000) return settings.PollIntervalMidMs;
|
//
|
||||||
return settings.PollIntervalFarMs;
|
// 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>
|
/// <summary>
|
||||||
|
|||||||
@@ -565,6 +565,47 @@ namespace AutoBidder.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsInSoftRetreat { get; set; } = false;
|
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>
|
/// <summary>
|
||||||
/// Contatore puntate effettuate in questa sessione su questa asta
|
/// Contatore puntate effettuate in questa sessione su questa asta
|
||||||
@@ -662,6 +703,13 @@ namespace AutoBidder.Models
|
|||||||
/// Override: abilita/disabilita soft retreat per questa asta
|
/// Override: abilita/disabilita soft retreat per questa asta
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? SoftRetreatEnabledOverride { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Override: limite puntate per questa asta
|
/// Override: limite puntate per questa asta
|
||||||
|
|||||||
@@ -14,11 +14,37 @@ namespace AutoBidder.Models
|
|||||||
[JsonPropertyName("Price")]
|
[JsonPropertyName("Price")]
|
||||||
public decimal Price { get; set; }
|
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>
|
/// <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>
|
/// </summary>
|
||||||
[JsonPropertyName("BidType")]
|
[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>
|
/// <summary>
|
||||||
/// Timestamp della puntata (Unix timestamp)
|
/// Timestamp della puntata (Unix timestamp)
|
||||||
@@ -55,7 +81,7 @@ namespace AutoBidder.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indica se la puntata è stata fatta dall'utente corrente
|
/// Indica se la puntata � stata fatta dall'utente corrente
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsMyBid { get; set; }
|
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>
|
/// <summary>Prezzo minimo sotto il quale non puntare. <c>null</c> = predefinito.</summary>
|
||||||
public double? MinPrice { get; set; }
|
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>
|
/// <summary>Prezzo oltre il quale lasciar perdere l'asta. <c>null</c> = predefinito.</summary>
|
||||||
public double? MaxPrice { get; set; }
|
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>
|
/// <summary>Aggiorna i limiti di ritmo a caldo, senza ricreare il trasporto.</summary>
|
||||||
public void Configure(double requestsPerSecond) => _limiter.SetRate(requestsPerSecond);
|
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>
|
/// <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)
|
public HttpRequestMessage BuildGet(string url, string? referer = null, bool ajax = true)
|
||||||
{
|
{
|
||||||
@@ -206,6 +253,7 @@ namespace AutoBidder.Net
|
|||||||
|
|
||||||
if (!critical)
|
if (!critical)
|
||||||
{
|
{
|
||||||
|
await RispettaFrenoAsync(ct).ConfigureAwait(false);
|
||||||
await _limiter.WaitAsync(ct).ConfigureAwait(false);
|
await _limiter.WaitAsync(ct).ConfigureAwait(false);
|
||||||
await _concurrency.WaitAsync(ct).ConfigureAwait(false);
|
await _concurrency.WaitAsync(ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -224,6 +272,12 @@ namespace AutoBidder.Net
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode) Interlocked.Increment(ref _requestsFailed);
|
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
|
return new HttpOutcome
|
||||||
{
|
{
|
||||||
Success = response.IsSuccessStatusCode,
|
Success = response.IsSuccessStatusCode,
|
||||||
@@ -304,9 +358,20 @@ namespace AutoBidder.Net
|
|||||||
private double _tokens;
|
private double _tokens;
|
||||||
private long _lastTicks;
|
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)
|
public TokenBucket(double ratePerSecond, int capacity)
|
||||||
{
|
{
|
||||||
_ratePerSecond = Math.Max(1, ratePerSecond);
|
_ratePerSecond = ratePerSecond;
|
||||||
_capacity = Math.Max(1, capacity);
|
_capacity = Math.Max(1, capacity);
|
||||||
_tokens = _capacity;
|
_tokens = _capacity;
|
||||||
_lastTicks = Stopwatch.GetTimestamp();
|
_lastTicks = Stopwatch.GetTimestamp();
|
||||||
@@ -316,8 +381,8 @@ namespace AutoBidder.Net
|
|||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
_ratePerSecond = Math.Max(1, ratePerSecond);
|
_ratePerSecond = ratePerSecond;
|
||||||
_capacity = Math.Max(8, _ratePerSecond);
|
_capacity = Math.Max(8, Math.Abs(_ratePerSecond));
|
||||||
if (_tokens > _capacity) _tokens = _capacity;
|
if (_tokens > _capacity) _tokens = _capacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,6 +393,8 @@ namespace AutoBidder.Net
|
|||||||
|
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
|
if (Illimitato) return ValueTask.CompletedTask;
|
||||||
|
|
||||||
var now = Stopwatch.GetTimestamp();
|
var now = Stopwatch.GetTimestamp();
|
||||||
var elapsedSeconds = (now - _lastTicks) / (double)Stopwatch.Frequency;
|
var elapsedSeconds = (now - _lastTicks) / (double)Stopwatch.Frequency;
|
||||||
_lastTicks = now;
|
_lastTicks = now;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -620,7 +620,7 @@ namespace AutoBidder.Services
|
|||||||
Username = state.LastBidder,
|
Username = state.LastBidder,
|
||||||
Price = statePrice,
|
Price = statePrice,
|
||||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
BidType = "Auto"
|
BidType = BidHistoryEntry.TipoSconosciuto
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1193,10 +1193,20 @@ namespace AutoBidder.Services
|
|||||||
Username = currentBidder,
|
Username = currentBidder,
|
||||||
Price = statePrice,
|
Price = statePrice,
|
||||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
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,
|
// I conteggi per utente restano a carico di UpdateBidderStatsFromRecentBids,
|
||||||
// unico punto che li tocca: contarli anche qui li raddoppierebbe.
|
// unico punto che li tocca: contarli anche qui li raddoppierebbe.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,47 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
return decision;
|
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!
|
// ? RIMOSSO: Entry Point - Era sbagliato!
|
||||||
// I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
// I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
||||||
// Se l'utente imposta MaxPrice=2�, vuole puntare FINO A 2�, non fino al 70%!
|
// Se l'utente imposta MaxPrice=2�, vuole puntare FINO A 2�, non fino al 70%!
|
||||||
@@ -466,6 +506,26 @@ namespace AutoBidder.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registra una puntata effettuata (per tracking)
|
/// Registra una puntata effettuata (per tracking)
|
||||||
/// </summary>
|
/// </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)
|
public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false)
|
||||||
{
|
{
|
||||||
auction.SessionBidCount++;
|
auction.SessionBidCount++;
|
||||||
@@ -475,6 +535,11 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
auction.SuccessfulBidCount++;
|
auction.SuccessfulBidCount++;
|
||||||
auction.ConsecutiveCollisions = 0;
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -192,8 +192,10 @@ namespace AutoBidder.Services
|
|||||||
var settings = config.PromoHarvest;
|
var settings = config.PromoHarvest;
|
||||||
var text = body ?? "";
|
var text = body ?? "";
|
||||||
|
|
||||||
if (FreeBidsPageParser.LooksLikeLogin(text, config))
|
// Il riconoscimento della pagina di accesso scattava anche su pagine che di
|
||||||
return new RedeemResult(code, false, "sessione scaduta: risposta la pagina di accesso");
|
// 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
|
var success = settings.SuccessMarkers
|
||||||
.FirstOrDefault(m => !string.IsNullOrWhiteSpace(m) &&
|
.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>
|
/// <summary>Oltre questa lunghezza la risposta è trattata come pagina, non come messaggio.</summary>
|
||||||
private const int FragmentMaxLength = 2000;
|
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(
|
public static FreeBidsResponseVerdict Validate(
|
||||||
FreeBidsSiteConfig config,
|
FreeBidsSiteConfig config,
|
||||||
int statusCode,
|
int statusCode,
|
||||||
string? body)
|
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();
|
config = (config ?? new FreeBidsSiteConfig()).Normalised();
|
||||||
|
|
||||||
|
|||||||
@@ -56,57 +56,89 @@ namespace AutoBidder.Services
|
|||||||
var failed = 0;
|
var failed = 0;
|
||||||
var skipped = 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)
|
var record = missing[i];
|
||||||
{
|
|
||||||
skipped++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
record.WinnerBidsPaid = state.WinnerBidsPaid;
|
|
||||||
record.WinnerBidsFree = state.WinnerBidsFree;
|
|
||||||
|
|
||||||
var check = AuctionIntegrity.CheckWinnerBids(record);
|
try
|
||||||
if (check.IsTrustworthy)
|
{
|
||||||
|
var state = await FetchAsync(record.AuctionId, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (state?.WinnerBidsTotal is null)
|
||||||
{
|
{
|
||||||
// Si riscrive solo il record toccato: Append sostituisce
|
skipped++;
|
||||||
// quello con lo stesso id.
|
|
||||||
CompletedAuctionsStore.Append(record);
|
|
||||||
updated++;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Il valore c'è ma non regge il confronto col prezzo: non si
|
record.WinnerBidsPaid = state.WinnerBidsPaid;
|
||||||
// salva, e si dice perché. Meglio un buco che una media storta.
|
record.WinnerBidsFree = state.WinnerBidsFree;
|
||||||
record.WinnerBidsPaid = null;
|
|
||||||
record.WinnerBidsFree = null;
|
var check = AuctionIntegrity.CheckWinnerBids(record);
|
||||||
skipped++;
|
if (check.IsTrustworthy)
|
||||||
OnLog?.Invoke($"[RECUPERO] {record.Name}: scartato ({check.Reason})");
|
{
|
||||||
|
// 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)
|
||||||
catch (OperationCanceledException)
|
{
|
||||||
{
|
break;
|
||||||
break;
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
failed++;
|
||||||
failed++;
|
OnLog?.Invoke($"[RECUPERO] {record.AuctionId}: {ex.Message}");
|
||||||
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);
|
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.Kept, kept.Count);
|
||||||
Assert.Equal(report.Removed, list.Count - kept.Count);
|
Assert.Equal(report.Removed, list.Count - kept.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Aste seguite solo in parte ───────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Il prezzo finale in centesimi e' il totale delle puntate dell'asta: confrontarlo
|
||||||
|
// con i reset osservati dice quanta asta si e' vista, senza bisogno di registrare
|
||||||
|
// nulla di nuovo. Vale quindi anche sullo storico raccolto prima della regola.
|
||||||
|
|
||||||
|
private static CompletedAuctionRecord Seguita(string id, double price, int resets)
|
||||||
|
{
|
||||||
|
var r = R(id: id, price: price, bidsFree: 1);
|
||||||
|
r.TotalResets = resets;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.00, 100, 1.0)] // 1,00 EUR = 100 puntate, viste tutte
|
||||||
|
[InlineData(1.00, 50, 0.5)] // agganciata a meta'
|
||||||
|
[InlineData(1.00, 0, 0.0)] // vista solo la fine
|
||||||
|
public void La_copertura_e_i_reset_sul_totale_di_puntate(
|
||||||
|
double price, int resets, double attesa)
|
||||||
|
{
|
||||||
|
Assert.Equal(attesa, AuctionIntegrity.ObservedCoverage(price, resets)!.Value, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Senza_prezzo_la_copertura_non_si_puo_dire()
|
||||||
|
{
|
||||||
|
Assert.Null(AuctionIntegrity.ObservedCoverage(0, 42));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_copertura_non_supera_mai_uno()
|
||||||
|
{
|
||||||
|
// L'arrotondamento del prezzo puo' produrre qualche reset di troppo: non e'
|
||||||
|
// un'asta osservata piu' del possibile, e non deve diventare 103%.
|
||||||
|
Assert.Equal(1.0, AuctionIntegrity.ObservedCoverage(1.00, 103)!.Value, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Toglie_le_aste_seguite_sotto_la_soglia()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord>
|
||||||
|
{
|
||||||
|
Seguita("intera", 1.00, 100),
|
||||||
|
Seguita("quasi", 1.00, 95),
|
||||||
|
Seguita("meta", 1.00, 50),
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new StatsMaintenance.CleanupOptions
|
||||||
|
{
|
||||||
|
MinObservedCoverage = 0.90
|
||||||
|
};
|
||||||
|
|
||||||
|
var kept = StatsMaintenance.Apply(list, options);
|
||||||
|
|
||||||
|
Assert.Equal(2, kept.Count);
|
||||||
|
Assert.DoesNotContain(kept, r => r.AuctionId == "meta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Spenta_la_soglia_non_toglie_nulla_per_copertura()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord> { Seguita("meta", 1.00, 1) };
|
||||||
|
|
||||||
|
var report = StatsMaintenance.Preview(list,
|
||||||
|
new StatsMaintenance.CleanupOptions { MinObservedCoverage = null });
|
||||||
|
|
||||||
|
Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,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]
|
[Fact]
|
||||||
public void Un_errore_http_non_e_un_riscatto()
|
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.False(verdict.Accepted);
|
||||||
Assert.Contains("500", verdict.Reason);
|
Assert.Contains("500", verdict.Reason);
|
||||||
@@ -473,7 +473,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Il_json_del_sito_ha_lultima_parola()
|
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!"}""");
|
"""{"success": true, "bids": 5, "message": "Hai riscattato 5 puntate!"}""");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -484,7 +484,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Un_rifiuto_con_200_resta_un_rifiuto()
|
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"}""");
|
"""{"success": false, "message": "Premio già riscattato"}""");
|
||||||
|
|
||||||
Assert.False(verdict.Accepted);
|
Assert.False(verdict.Accepted);
|
||||||
@@ -496,7 +496,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
{
|
{
|
||||||
// Chiedere all'utente di indovinare il percorso completo sarebbe un modo per
|
// Chiedere all'utente di indovinare il percorso completo sarebbe un modo per
|
||||||
// fargli sbagliare la configurazione.
|
// fargli sbagliare la configurazione.
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||||
"""{"status": 200, "data": {"success": "true", "bids": "3"}}""");
|
"""{"status": 200, "data": {"success": "true", "bids": "3"}}""");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -511,7 +511,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
config.ResponseValidation.SuccessValue = "ok";
|
config.ResponseValidation.SuccessValue = "ok";
|
||||||
config.ResponseValidation.BidsCountKey = "bids_added";
|
config.ResponseValidation.BidsCountKey = "bids_added";
|
||||||
|
|
||||||
var verdict = FreeBidsResponseValidator.Validate(config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(config, 200,
|
||||||
"""{"esito": "ok", "bids_added": 12}""");
|
"""{"esito": "ok", "bids_added": 12}""");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -523,7 +523,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
{
|
{
|
||||||
// È il caso peggiore: sessione scaduta, HTTP 200, e ogni giro conterebbe premi
|
// È il caso peggiore: sessione scaduta, HTTP 200, e ogni giro conterebbe premi
|
||||||
// che non sono mai stati presi.
|
// 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>");
|
"<html><body><form action=\"/login.php\"><input name=\"password\"></form></body></html>");
|
||||||
|
|
||||||
Assert.False(verdict.Accepted);
|
Assert.False(verdict.Accepted);
|
||||||
@@ -533,7 +533,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Una_pagina_intera_vale_come_accettata_e_le_puntate_si_leggono_dal_testo()
|
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>");
|
"<html><body><h1>Complimenti! Hai ricevuto 3 puntate</h1></body></html>");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -543,7 +543,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void In_un_messaggio_breve_le_parole_di_rifiuto_contano()
|
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.False(verdict.Accepted);
|
||||||
Assert.Contains("rifiutato", verdict.Reason);
|
Assert.Contains("rifiutato", verdict.Reason);
|
||||||
@@ -552,7 +552,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Una_risposta_vuota_si_accetta_e_decide_il_saldo()
|
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.True(verdict.Accepted);
|
||||||
Assert.Null(verdict.BidsDeclared);
|
Assert.Null(verdict.BidsDeclared);
|
||||||
@@ -794,3 +794,60 @@ public class FreeBidsStatsTests
|
|||||||
Assert.Equal(7, FreeBidsStats.Read().TotalBids);
|
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="AlternatingRowBackground" Value="{DynamicResource Brush.Surface}"/>
|
||||||
<Setter Property="GridLinesVisibility" Value="None"/>
|
<Setter Property="GridLinesVisibility" Value="None"/>
|
||||||
<Setter Property="HeadersVisibility" Value="Column"/>
|
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||||
<Setter Property="RowHeight" Value="28"/>
|
<!-- Densità: 22 px invece di 28 fanno entrare circa un quarto di righe in più a
|
||||||
<Setter Property="ColumnHeaderHeight" Value="30"/>
|
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="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="CanUserAddRows" Value="False"/>
|
||||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||||
<Setter Property="SelectionMode" Value="Single"/>
|
<Setter Property="SelectionMode" Value="Single"/>
|
||||||
<Setter Property="EnableRowVirtualization" Value="True"/>
|
<Setter Property="EnableRowVirtualization" Value="True"/>
|
||||||
|
<!-- Il riciclo riusa i contenitori di riga invece di ricrearli a ogni giro di
|
||||||
|
scorrimento. Con la griglia del monitor, che si aggiorna piu' volte al
|
||||||
|
secondo mentre le aste corrono, e' la differenza fra allocare righe nuove
|
||||||
|
di continuo e non allocarne quasi nessuna. Il valore predefinito di WPF e'
|
||||||
|
"Standard", cioe' senza riciclo. -->
|
||||||
|
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
|
||||||
<Setter Property="VerticalGridLinesBrush" Value="Transparent"/>
|
<Setter Property="VerticalGridLinesBrush" Value="Transparent"/>
|
||||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@@ -63,5 +63,33 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
||||||
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
|
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quanta parte dell'asta è stata davvero osservata, da 0 a 1. <c>null</c> se non
|
||||||
|
/// si può dire (prezzo finale mancante).
|
||||||
|
///
|
||||||
|
/// <para>Sfrutta la stessa identità del controllo qui sopra: il prezzo finale in
|
||||||
|
/// centesimi è il numero totale di puntate dell'asta. Confrontarlo con i reset
|
||||||
|
/// visti dice, senza bisogno di registrare nient'altro, se l'asta è stata seguita
|
||||||
|
/// dall'inizio o presa per strada. Un'asta agganciata a metà ha numeri veri ma
|
||||||
|
/// parziali — è il caso peggiore, perché sembra buona.</para>
|
||||||
|
///
|
||||||
|
/// <para>Il valore è tagliato a 1: sui dati reali (5876 aste) nessun record supera
|
||||||
|
/// 1,02, e il poco che sfora è arrotondamento del prezzo, non un'asta osservata
|
||||||
|
/// più del possibile.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static double? ObservedCoverage(double finalPrice, int totalResets)
|
||||||
|
{
|
||||||
|
if (finalPrice <= 0) return null;
|
||||||
|
|
||||||
|
var totalBids = Math.Round(finalPrice * 100);
|
||||||
|
if (totalBids <= 0) return null;
|
||||||
|
|
||||||
|
return Math.Min(1.0, Math.Max(0, totalResets) / totalBids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Comodità: la copertura di un'asta già registrata.</summary>
|
||||||
|
public static double? ObservedCoverage(CompletedAuctionRecord record) =>
|
||||||
|
record == null ? null : ObservedCoverage(record.FinalPrice, record.TotalResets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
// ── Copia in memoria ─────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Il file cresce di un record per asta e arriva in fretta a qualche megabyte:
|
||||||
|
// a 5876 aste, leggerlo e deserializzarlo costa ~72 ms misurati. Senza copia
|
||||||
|
// in memoria quel prezzo si paga a ogni apertura delle statistiche e a ogni
|
||||||
|
// asta che finisce — e quella chiamata arriva da un gestore di eventi, cioe'
|
||||||
|
// dal thread dell'interfaccia, che resta fermo per tutto il tempo.
|
||||||
|
//
|
||||||
|
// L'invalidazione guarda la data di modifica del file, non un tempo di
|
||||||
|
// scadenza: cosi' la copia resta giusta anche se il file viene sostituito da
|
||||||
|
// fuori — ripristino di un backup, modifica a mano — che qui capita davvero.
|
||||||
|
//
|
||||||
|
// Il prezzo da sapere: i record restituiti sono gli STESSI oggetti che stanno
|
||||||
|
// nella copia. Chi ne modifica uno sta modificando anche la copia, prima e
|
||||||
|
// indipendentemente dal salvataggio. Repair e il recupero puntate contano su
|
||||||
|
// questo; chi non lo vuole si faccia una copia del record.
|
||||||
|
private static List<CompletedAuctionRecord>? _cache;
|
||||||
|
private static DateTime _cacheStamp;
|
||||||
|
|
||||||
/// <summary>Percorso del file storico (per mostrarlo/aprirlo nella UI).</summary>
|
/// <summary>Percorso del file storico (per mostrarlo/aprirlo nella UI).</summary>
|
||||||
public static string StorePath => FilePath;
|
public static string StorePath => FilePath;
|
||||||
|
|
||||||
@@ -34,16 +53,50 @@ namespace AutoBidder.Utilities
|
|||||||
public static void Append(CompletedAuctionRecord record)
|
public static void Append(CompletedAuctionRecord record)
|
||||||
{
|
{
|
||||||
if (record == null) return;
|
if (record == null) return;
|
||||||
|
AppendMany(new[] { record });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiunge o sostituisce piu' aste con <b>un solo</b> salvataggio.
|
||||||
|
///
|
||||||
|
/// <para>Serve a chi ne tocca tante di fila, come il recupero delle puntate dei
|
||||||
|
/// vincitori. Chiamare <see cref="Append"/> in un ciclo rilegge e riscrive tutto
|
||||||
|
/// il file a ogni giro: sulle 4675 aste di un recupero completo erano minuti di
|
||||||
|
/// solo rimescolamento JSON e svariati gigabyte scritti sul disco per nulla.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendMany(IReadOnlyCollection<CompletedAuctionRecord> records)
|
||||||
|
{
|
||||||
|
if (records == null || records.Count == 0) return;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var list = LoadInternal();
|
var list = LoadInternal();
|
||||||
|
|
||||||
var existing = list.FindIndex(r =>
|
// Indice per id: cercare ogni record con un FindIndex renderebbe
|
||||||
!string.IsNullOrEmpty(r.AuctionId) && r.AuctionId == record.AuctionId);
|
// quadratico proprio il caso per cui questo metodo esiste.
|
||||||
|
var byId = new Dictionary<string, int>(list.Count, StringComparer.Ordinal);
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
var id = list[i].AuctionId;
|
||||||
|
if (!string.IsNullOrEmpty(id)) byId[id] = i;
|
||||||
|
}
|
||||||
|
|
||||||
if (existing >= 0) list[existing] = record;
|
foreach (var record in records)
|
||||||
else list.Add(record);
|
{
|
||||||
|
if (record == null) continue;
|
||||||
|
|
||||||
|
var id = record.AuctionId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(id) && byId.TryGetValue(id, out var at))
|
||||||
|
{
|
||||||
|
list[at] = record;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(id)) byId[id] = list.Count;
|
||||||
|
list.Add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SaveInternal(list);
|
SaveInternal(list);
|
||||||
}
|
}
|
||||||
@@ -119,10 +172,12 @@ namespace AutoBidder.Utilities
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(FilePath)) File.Delete(FilePath);
|
if (File.Exists(FilePath)) File.Delete(FilePath);
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
|
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,18 +276,33 @@ namespace AutoBidder.Utilities
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(FilePath))
|
if (!File.Exists(FilePath))
|
||||||
|
{
|
||||||
|
_cache = null;
|
||||||
return new List<CompletedAuctionRecord>();
|
return new List<CompletedAuctionRecord>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Una lettura di data di modifica costa microsecondi; la lettura piu'
|
||||||
|
// la deserializzazione che evita ne costa decine di millisecondi.
|
||||||
|
var stamp = File.GetLastWriteTimeUtc(FilePath);
|
||||||
|
if (_cache != null && stamp == _cacheStamp) return _cache;
|
||||||
|
|
||||||
var json = File.ReadAllText(FilePath);
|
var json = File.ReadAllText(FilePath);
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
|
||||||
return new List<CompletedAuctionRecord>();
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
var list = string.IsNullOrWhiteSpace(json)
|
||||||
?? new List<CompletedAuctionRecord>();
|
? new List<CompletedAuctionRecord>()
|
||||||
|
: JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
||||||
|
?? new List<CompletedAuctionRecord>();
|
||||||
|
|
||||||
|
_cache = list;
|
||||||
|
_cacheStamp = stamp;
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore lettura storico: {ex.Message}");
|
Console.WriteLine($"[STATS] Errore lettura storico: {ex.Message}");
|
||||||
|
|
||||||
|
// Non si sa cosa ci sia sul disco: la prossima volta si rilegge.
|
||||||
|
_cache = null;
|
||||||
return new List<CompletedAuctionRecord>();
|
return new List<CompletedAuctionRecord>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,10 +316,16 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
var json = JsonSerializer.Serialize(list, JsonOptions);
|
var json = JsonSerializer.Serialize(list, JsonOptions);
|
||||||
File.WriteAllText(FilePath, json);
|
File.WriteAllText(FilePath, json);
|
||||||
|
|
||||||
|
// La copia in memoria e' esattamente quello che si e' appena scritto:
|
||||||
|
// tenerla evita di rileggere il file al primo uso successivo.
|
||||||
|
_cache = list;
|
||||||
|
_cacheStamp = File.GetLastWriteTimeUtc(FilePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore salvataggio storico: {ex.Message}");
|
Console.WriteLine($"[STATS] Errore salvataggio storico: {ex.Message}");
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,21 @@ namespace AutoBidder.Utilities
|
|||||||
/// dossier non possono dire di piu', perche' Bidoo dichiara la scadenza al secondo
|
/// dossier non possono dire di piu', perche' Bidoo dichiara la scadenza al secondo
|
||||||
/// intero.</para>
|
/// intero.</para>
|
||||||
/// </summary>
|
/// </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 DefaultMinPrice { get; set; } = 0;
|
||||||
public double DefaultMaxPrice { get; set; } = 0;
|
public double DefaultMaxPrice { get; set; } = 0;
|
||||||
public int DefaultMaxClicks { 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
|
/// Tetto complessivo di richieste al secondo verso Bidoo (le puntate non sono soggette
|
||||||
/// al limite: hanno corsia preferenziale). Default: 40.
|
/// al limite: hanno corsia preferenziale). Default: 40.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Porta la granularità del timer di sistema a 1 ms mentre il monitor è in funzione.
|
/// 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)
|
/// Default: false (DISABILITATO - non blocca mai)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
|
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
|
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||||||
|
|||||||
@@ -100,6 +100,17 @@ namespace AutoBidder.Utilities
|
|||||||
/// <summary>Doppioni con lo stesso id: si tiene il più recente.</summary>
|
/// <summary>Doppioni con lo stesso id: si tiene il più recente.</summary>
|
||||||
public bool RemoveDuplicates { get; set; } = true;
|
public bool RemoveDuplicates { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copertura minima richiesta, da 0 a 1: sotto questa soglia l'asta è stata
|
||||||
|
/// agganciata a metà e va tolta. Null = non filtrare sulla copertura.
|
||||||
|
///
|
||||||
|
/// <para>È il criterio per «non seguita dall'inizio alla fine». Non serve
|
||||||
|
/// nessun campo nuovo nei record: la copertura si ricava dal prezzo finale,
|
||||||
|
/// vedi <see cref="AuctionIntegrity.ObservedCoverage(double,int)"/>. Vale
|
||||||
|
/// quindi anche per lo storico raccolto prima che questa regola esistesse.</para>
|
||||||
|
/// </summary>
|
||||||
|
public double? MinObservedCoverage { get; set; }
|
||||||
|
|
||||||
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
||||||
public DateTime? OlderThan { get; set; }
|
public DateTime? OlderThan { get; set; }
|
||||||
}
|
}
|
||||||
@@ -161,6 +172,15 @@ namespace AutoBidder.Utilities
|
|||||||
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
|
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
|
||||||
Mark(r, "senza valore del prodotto");
|
Mark(r, "senza valore del prodotto");
|
||||||
|
|
||||||
|
if (options.MinObservedCoverage is { } soglia)
|
||||||
|
{
|
||||||
|
// Copertura non calcolabile = asta senza prezzo finale: qui vale come
|
||||||
|
// "non seguita", perché non c'è modo di dire il contrario.
|
||||||
|
var copertura = AuctionIntegrity.ObservedCoverage(r);
|
||||||
|
if (copertura is null || copertura.Value < soglia)
|
||||||
|
Mark(r, "seguita solo in parte");
|
||||||
|
}
|
||||||
|
|
||||||
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
||||||
Mark(r, "più vecchia del limite");
|
Mark(r, "più vecchia del limite");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -500,8 +500,29 @@ namespace AutoBidder.ViewModels
|
|||||||
OnPropertyChanged(nameof(LatencyDisplay)); // ? AGGIUNTO - latenza cambia quando si attiva/disattiva
|
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;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
protected void OnPropertyChanged(string propertyName)
|
protected void OnPropertyChanged(string propertyName)
|
||||||
{
|
{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ DisableProgramGroupPage=yes
|
|||||||
DisableDirPage=auto
|
DisableDirPage=auto
|
||||||
|
|
||||||
OutputDir={#OutputDir}
|
OutputDir={#OutputDir}
|
||||||
OutputBaseFilename=AutoBidder-{#AppVersion}-setup
|
OutputBaseFilename=AutoBidder_{#AppVersion}
|
||||||
SetupIconFile=..\Icon\favicon.ico
|
SetupIconFile=..\Icon\favicon.ico
|
||||||
UninstallDisplayIcon={app}\{#AppExeName}
|
UninstallDisplayIcon={app}\{#AppExeName}
|
||||||
UninstallDisplayName={#AppName} {#AppVersion}
|
UninstallDisplayName={#AppName} {#AppVersion}
|
||||||
|
|||||||
@@ -465,7 +465,7 @@
|
|||||||
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Setup>$(CartellaPacchetti)\AutoBidder-$(V)-setup.exe</Setup>
|
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
||||||
@@ -540,7 +540,7 @@
|
|||||||
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
||||||
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
||||||
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
||||||
<Setup>$(CartellaPacchetti)\AutoBidder-$(V)-setup.exe</Setup>
|
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<MakeDir Directories="$(Tmp)" />
|
<MakeDir Directories="$(Tmp)" />
|
||||||
|
|||||||
Reference in New Issue
Block a user