Sostituisci le euristiche a soglia con regime e anticipo adattivi; un solo archivio per asta
Le strategie a numero fisso (calore, ritiro morbido, anti-bot, avversari aggressivi, puntata probabilistica, velocità del prezzo, esaurimento, concorrenza) e le impostazioni morte (cadenze di polling, finestra critica, database, log avanzati, suggerimenti sull'anticipo, schede dettagliate) non ci sono più: rigiocate sui dossier non fermavano una puntata sbagliata senza fermarne anche di giuste, e i loro numeri andavano tarati a mano. Restano i paletti dell'utente (tetti, budget, fascia oraria) e il duello. Al loro posto due componenti che imparano dalla sessione in corso, dentro i paletti: - CompetitionRegime per asta (Calmo / Sfogo / Sondaggio) guidato dal valore atteso appreso: tre negativi di fila e si lascia sfogare gli altri, si rientra con una puntata di prova dopo abbastanza cicli buoni, e se viene coperta subito la pazienza raddoppia. È il "capire da soli quando tornare a puntare". - LatencyModel per l'anticipo: margine + p99 della latenza misurata adesso, tenuto fra LeadMinMs e LeadMaxMs; una puntata tardiva alza il margine subito, venti in tempo lo abbassano piano. Un anticipo scritto a mano su un'asta vince sempre (BidLeadIsManual). Archiviazione: l'archivio mensile aste-AAAA-MM.jsonl (95 MB) duplicava il riepilogo che ogni dossier ha già in coda. AuctionDetailStore ora legge testa e coda dei dossier, con cache: 6349 file in 6 s la prima volta. Pulsanti per svuotare le esportazioni e per azzerare le statistiche voce per voce (lo storico passa dalla copia di sicurezza). Scheda Apprendimento: sezione "Autonomia sul momento" con il modello di latenza e il regime di ogni asta seguita. Ml/LEGGIMI.md descrive l'algoritmo per intero. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,13 @@
|
|||||||
Content="Apri la cartella" Margin="0,0,8,0"
|
Content="Apri la cartella" Margin="0,0,8,0"
|
||||||
Click="OpenFolderButton_Click"/>
|
Click="OpenFolderButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="ClearExportsButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
|
Content="Svuota la cartella" Margin="0,0,8,0"
|
||||||
|
ToolTip="Cancella tutti i file esportati finora. Sono copie: si rigenerano in qualunque momento da qui."
|
||||||
|
Click="ClearExportsButton_Click"/>
|
||||||
|
|
||||||
<Button x:Name="ResetFiltersButton" Style="{StaticResource IconButton}"
|
<Button x:Name="ResetFiltersButton" Style="{StaticResource IconButton}"
|
||||||
ToolTip="Azzera i filtri" Click="ResetFiltersButton_Click">
|
ToolTip="Azzera i filtri" Click="ResetFiltersButton_Click">
|
||||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ namespace AutoBidder.Controls
|
|||||||
private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
|
private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
=> RaiseEvent(new RoutedEventArgs(OpenExportFolderClickedEvent, this));
|
=> RaiseEvent(new RoutedEventArgs(OpenExportFolderClickedEvent, this));
|
||||||
|
|
||||||
|
private void ClearExportsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ClearExportsClickedEvent, this));
|
||||||
|
|
||||||
private void OpenLastFileButton_Click(object sender, RoutedEventArgs e)
|
private void OpenLastFileButton_Click(object sender, RoutedEventArgs e)
|
||||||
=> RaiseEvent(new RoutedEventArgs(OpenLastFileClickedEvent, this));
|
=> RaiseEvent(new RoutedEventArgs(OpenLastFileClickedEvent, this));
|
||||||
|
|
||||||
@@ -171,6 +174,15 @@ namespace AutoBidder.Controls
|
|||||||
public static readonly RoutedEvent OpenLastFileClickedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent OpenLastFileClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"OpenLastFileClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
"OpenLastFileClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ClearExportsClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ClearExportsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
||||||
|
|
||||||
|
public event RoutedEventHandler ClearExportsClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ClearExportsClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ClearExportsClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public event RoutedEventHandler ExportRequested
|
public event RoutedEventHandler ExportRequested
|
||||||
{
|
{
|
||||||
add { AddHandler(ExportRequestedEvent, value); }
|
add { AddHandler(ExportRequestedEvent, value); }
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Stato -->
|
<!-- Stato -->
|
||||||
@@ -212,6 +213,72 @@
|
|||||||
Text="Nessuna valutazione ancora. Premi «Valuta ora»."/>
|
Text="Nessuna valutazione ancora. Premi «Valuta ora»."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Autonomia sul momento -->
|
||||||
|
<Border Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Autonomia sul momento" Margin="0,0,0,10"
|
||||||
|
Help="Due cose che il sistema decide da solo sulla sessione in corso, dentro i paletti delle impostazioni. ANTICIPO: margine di sicurezza più la coda alta (p99) della latenza misurata adesso. Una puntata arrivata tardi alza il margine di 150 ms subito; venti puntate in tempo lo abbassano di 25. Il risultato resta fra il minimo e il massimo impostati; un anticipo scritto a mano su una singola asta vince sempre. REGIME: per ogni asta seguita. Calmo = punta se il valore atteso è positivo. Sfogo = gli altri si stanno battendo, si aspetta che si sfoghino: si rientra solo dopo «pazienza» cicli buoni di fila. Sondaggio = una puntata di prova; se viene coperta entro 9 s si torna a Sfogo e la pazienza raddoppia (fino a 24)."/>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="2*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="Anticipo adattivo" FontWeight="SemiBold" Margin="0,0,0,8"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
<UniformGrid Columns="3" Rows="2">
|
||||||
|
<StackPanel Margin="0,0,8,10">
|
||||||
|
<TextBlock x:Name="StatLeadNow" Style="{StaticResource Stat}" Text="—"/>
|
||||||
|
<TextBlock Style="{StaticResource StatLabel}" Text="anticipo adesso"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,8,10">
|
||||||
|
<TextBlock x:Name="StatLatP99" Style="{StaticResource Stat}" Text="—"/>
|
||||||
|
<TextBlock Style="{StaticResource StatLabel}" Text="coda del ping (p99)"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,8,10">
|
||||||
|
<TextBlock x:Name="StatLatMargin" Style="{StaticResource Stat}" Text="—"/>
|
||||||
|
<TextBlock Style="{StaticResource StatLabel}" Text="margine"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,8,0">
|
||||||
|
<TextBlock x:Name="StatLatP50" Style="{StaticResource Stat}" Text="—"/>
|
||||||
|
<TextBlock Style="{StaticResource StatLabel}" Text="ping mediano"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,8,0">
|
||||||
|
<TextBlock x:Name="StatLatSamples" Style="{StaticResource Stat}" Text="—"/>
|
||||||
|
<TextBlock Style="{StaticResource StatLabel}" Text="campioni"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,8,0">
|
||||||
|
<TextBlock x:Name="StatLatLate" Style="{StaticResource Stat}" Text="—"/>
|
||||||
|
<TextBlock Style="{StaticResource StatLabel}" Text="puntate tardive / totali"/>
|
||||||
|
</StackPanel>
|
||||||
|
</UniformGrid>
|
||||||
|
<TextBlock x:Name="StatLeadHint" Style="{StaticResource Hint}" Margin="0,8,0,0" Text=""/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2">
|
||||||
|
<TextBlock Text="Regime per asta" FontWeight="SemiBold" Margin="0,0,0,8"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
<DataGrid x:Name="RegimeGrid" MaxHeight="240" IsReadOnly="True" HeadersVisibility="Column"
|
||||||
|
AutoGenerateColumns="False" CanUserAddRows="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Asta" Binding="{Binding Asta}" Width="*"/>
|
||||||
|
<DataGridTextColumn Header="Stato" Binding="{Binding StatoAsta}" Width="Auto" MinWidth="60"/>
|
||||||
|
<DataGridTextColumn Header="Regime" Binding="{Binding Regime}" Width="Auto" MinWidth="80"/>
|
||||||
|
<DataGridTextColumn Header="Pazienza" Binding="{Binding Pazienza}" Width="Auto" MinWidth="70"/>
|
||||||
|
<DataGridTextColumn Header="Sondaggi coperti" Binding="{Binding Sondaggi}" Width="Auto" MinWidth="110"/>
|
||||||
|
<DataGridTextColumn Header="P senza risposta" Binding="{Binding Prob}" Width="Auto" MinWidth="110"/>
|
||||||
|
<DataGridTextColumn Header="Valore atteso" Binding="{Binding Ev}" Width="Auto" MinWidth="100"/>
|
||||||
|
<DataGridTextColumn Header="Duello" Binding="{Binding Duello}" Width="Auto" MinWidth="60"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
<TextBlock x:Name="RegimeHint" Style="{StaticResource Hint}" Margin="0,8,0,0" Text=""/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -44,6 +44,24 @@ namespace AutoBidder.Controls
|
|||||||
public string Chiusura { get; init; } = "";
|
public string Chiusura { get; init; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RegimeRow
|
||||||
|
{
|
||||||
|
public string Asta { get; init; } = "";
|
||||||
|
public string StatoAsta { get; init; } = "";
|
||||||
|
public string Regime { get; init; } = "";
|
||||||
|
public string Pazienza { get; init; } = "";
|
||||||
|
public string Sondaggi { get; init; } = "";
|
||||||
|
public string Prob { get; init; } = "";
|
||||||
|
public string Ev { get; init; } = "";
|
||||||
|
public string Duello { get; init; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Da dove prendere le aste seguite adesso, per la tabella dei regimi. Lo imposta
|
||||||
|
/// la finestra principale: la scheda non conosce il monitor, e non deve.
|
||||||
|
/// </summary>
|
||||||
|
public Func<IReadOnlyList<Models.AuctionInfo>>? AuctionsProvider { get; set; }
|
||||||
|
|
||||||
private sealed class DecisioneRow
|
private sealed class DecisioneRow
|
||||||
{
|
{
|
||||||
public string Ora { get; init; } = "";
|
public string Ora { get; init; } = "";
|
||||||
@@ -92,6 +110,8 @@ namespace AutoBidder.Controls
|
|||||||
StatThreshold.Text = soglia.ToString("N0");
|
StatThreshold.Text = soglia.ToString("N0");
|
||||||
StatBootstrap.Text = LearningService.BootstrapRunning ? "in corso" : "fermo";
|
StatBootstrap.Text = LearningService.BootstrapRunning ? "in corso" : "fermo";
|
||||||
|
|
||||||
|
RefreshAutonomy(settings);
|
||||||
|
|
||||||
var decisioni = LearningService.RecentDecisions();
|
var decisioni = LearningService.RecentDecisions();
|
||||||
var fermate = decisioni.Count(d => d.Blocked);
|
var fermate = decisioni.Count(d => d.Blocked);
|
||||||
StatDecisions.Text = decisioni.Count == 0
|
StatDecisions.Text = decisioni.Count == 0
|
||||||
@@ -145,6 +165,54 @@ namespace AutoBidder.Controls
|
|||||||
0 => "0-8", 1 => "9", 2 => "10-12", 3 => "13-17", 4 => "18-20", _ => "21-23"
|
0 => "0-8", 1 => "9", 2 => "10-12", 3 => "13-17", 4 => "18-20", _ => "21-23"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>La sezione «Autonomia sul momento»: modello di latenza e regime per asta.</summary>
|
||||||
|
private void RefreshAutonomy(AppSettings settings)
|
||||||
|
{
|
||||||
|
var lat = LatencyModel.Snapshot();
|
||||||
|
var lead = LatencyModel.RecommendedLeadMs(settings);
|
||||||
|
|
||||||
|
StatLeadNow.Text = $"{lead} ms";
|
||||||
|
StatLatP99.Text = lat.Samples > 0 ? $"{lat.P99} ms" : "—";
|
||||||
|
StatLatP50.Text = lat.Samples > 0 ? $"{lat.P50} ms" : "—";
|
||||||
|
StatLatMargin.Text = $"{lat.MarginMs} ms";
|
||||||
|
StatLatSamples.Text = lat.Samples.ToString("N0");
|
||||||
|
StatLatLate.Text = $"{lat.LateBids} / {lat.Bids}";
|
||||||
|
|
||||||
|
StatLeadHint.Text = !settings.AdaptiveLeadEnabled
|
||||||
|
? $"Anticipo adattivo spento nelle impostazioni: vale l'anticipo fisso di {settings.DefaultBidBeforeDeadlineMs} ms."
|
||||||
|
: lat.Samples < 10
|
||||||
|
? $"Ancora pochi campioni: finché non ce ne sono dieci vale l'anticipo fisso di {settings.DefaultBidBeforeDeadlineMs} ms."
|
||||||
|
: $"Margine {lat.MarginMs} + coda {lat.P99} = {lat.MarginMs + lat.P99} ms, tenuto fra {settings.LeadMinMs} e {settings.LeadMaxMs}.";
|
||||||
|
|
||||||
|
var aste = AuctionsProvider?.Invoke() ?? Array.Empty<Models.AuctionInfo>();
|
||||||
|
var righe = aste
|
||||||
|
.Where(a => a.State != Models.RunState.Stopped)
|
||||||
|
.Select(a => new RegimeRow
|
||||||
|
{
|
||||||
|
Asta = a.Name,
|
||||||
|
StatoAsta = a.State == Models.RunState.Active ? "Attiva" : "Osserva",
|
||||||
|
Regime = a.Regime.StatoAttuale switch
|
||||||
|
{
|
||||||
|
CompetitionRegime.Stato.Sfogo => $"Sfogo ({a.Regime.CicliBuoni}/{a.Regime.Pazienza})",
|
||||||
|
CompetitionRegime.Stato.Sondaggio => "Sondaggio",
|
||||||
|
_ => "Calmo"
|
||||||
|
},
|
||||||
|
Pazienza = a.Regime.Pazienza.ToString(),
|
||||||
|
Sondaggi = a.Regime.SondaggiFalliti.ToString(),
|
||||||
|
Prob = a.LearnedUnansweredProbability is { } p ? p.ToString("P1") : "—",
|
||||||
|
Ev = a.LearnedExpectedValue is { } ev ? ev.ToString("+0.000;-0.000") + " €" : "—",
|
||||||
|
Duello = a.AutoBidDuelDetected ? "sì" : a.AutoResponsesInARow > 0 ? $"{a.AutoResponsesInARow}/5" : ""
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
RegimeGrid.ItemsSource = righe;
|
||||||
|
|
||||||
|
var sfogo = righe.Count(r => r.Regime.StartsWith("Sfogo"));
|
||||||
|
RegimeHint.Text = righe.Count == 0
|
||||||
|
? "Nessuna asta seguita in questo momento."
|
||||||
|
: $"{righe.Count} aste seguite: {sfogo} in Sfogo, {righe.Count(r => r.Regime == "Sondaggio")} in Sondaggio, {righe.Count - sfogo - righe.Count(r => r.Regime == "Sondaggio")} Calme.";
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshButton_Click(object sender, RoutedEventArgs e) => Refresh();
|
private void RefreshButton_Click(object sender, RoutedEventArgs e) => Refresh();
|
||||||
|
|
||||||
private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
|
private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
|||||||
@@ -140,20 +140,32 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo Puntata (millisecondi)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" 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 (il ping tocca 444 ms nel p99,9). Fascia consigliata: 600–2000 ms. Predefinito 1000."/>
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo deciso dalla rete" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Acceso: l'anticipo lo sceglie il modello di latenza — margine di sicurezza più la coda alta del ping di questa sessione — e si corregge da solo a ogni puntata tardiva. Si muove solo fra il minimo e il massimo qui sotto. Spento: vale l'anticipo fisso della riga seguente. In entrambi i casi un anticipo scritto a mano su una singola asta vince sempre."/>
|
||||||
<TextBox Grid.Row="0" Grid.Column="1" x:Name="DefaultBidBeforeDeadlineMsTextBox" Text="200" Margin="10,10"/>
|
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="AdaptiveLeadCheckBox" Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Anticipo minimo (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Il modello non scende mai sotto questo valore. Sotto i 300 ms un picco di rete fa arrivare la puntata a giochi chiusi: il ping tocca 444 ms nel p99,9."/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="LeadMinMsTextBox" Text="300" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Anticipo massimo (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Il modello non sale mai sopra questo valore. Ogni millisecondo in più è tempo regalato agli avversari: dalla rigiocata di 2.095 aste, sotto il secondo basta in mediana 1 puntata per asta, sotto i 2 s ne servono 15."/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" x:Name="LeadMaxMsTextBox" Text="1500" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Anticipo fisso (millisecondi)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Usato quando l'anticipo deciso dalla rete è spento, e come punto di partenza finché il modello non ha misurato abbastanza ping. Quanti millisecondi prima della scadenza parte la puntata: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato."/>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" x:Name="DefaultBidBeforeDeadlineMsTextBox" Text="500" Margin="10,10"/>
|
||||||
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Prezzo Minimo (€)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="5" Grid.Column="0" Text="Prezzo Minimo (€)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
<TextBox Grid.Row="2" Grid.Column="1" x:Name="DefaultMinPriceTextBox" Text="0" Margin="10,10"/>
|
<TextBox Grid.Row="5" Grid.Column="1" x:Name="DefaultMinPriceTextBox" Text="0" Margin="10,10"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Prezzo Massimo (€)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="6" Grid.Column="0" Text="Prezzo Massimo (€)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
<TextBox Grid.Row="3" Grid.Column="1" x:Name="DefaultMaxPriceTextBox" Text="0" Margin="10,10"/>
|
<TextBox Grid.Row="6" Grid.Column="1" x:Name="DefaultMaxPriceTextBox" Text="0" Margin="10,10"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Max Click" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Quante puntate al massimo spendere su una singola asta. Entra anche nel calcolo dei limiti consigliati per prodotto: ogni puntata in più è denaro che non si può mettere sul prezzo. 0 = illimitate."/>
|
<TextBlock Grid.Row="7" Grid.Column="0" Text="Max Click" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Quante puntate al massimo spendere su una singola asta. Entra anche nel calcolo dei limiti consigliati per prodotto: ogni puntata in più è denaro che non si può mettere sul prezzo. 0 = illimitate."/>
|
||||||
<TextBox Grid.Row="4" Grid.Column="1" x:Name="DefaultMaxClicksTextBox" Text="0" Margin="10,10"/>
|
<TextBox Grid.Row="7" Grid.Column="1" x:Name="DefaultMaxClicksTextBox" Text="0" Margin="10,10"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -257,7 +269,7 @@ Consiglio: usa Fermata per caricare senza avviare, poi avvia a mano quelle che v
|
|||||||
<StackPanel>
|
<StackPanel>
|
||||||
<ctl:HelpHeader Title="Motore di Precisione"
|
<ctl:HelpHeader Title="Motore di Precisione"
|
||||||
Margin="0,0,0,14"
|
Margin="0,0,0,14"
|
||||||
Help="Ogni asta ha il proprio motore indipendente. Il polling si infittisce solo avvicinandosi alla scadenza: un'asta lontana non consuma richieste inutili, una vicina sì."/>
|
Help="Ogni asta ha il proprio motore indipendente e interroga Bidoo al massimo che la rete concede: nessuna cadenza da impostare. Se il server chiede di rallentare (429/503) il motore frena da solo e riparte appena può."/>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -267,33 +279,13 @@ Consiglio: usa Fermata per caricare senza avviare, poi avvia a mano quelle che v
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Polling oltre 60s (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Cadenza quando la scadenza è lontana"/>
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Limite richieste al secondo" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="0 = nessun tetto (consigliato: si va al massimo che la rete concede). Le puntate hanno corsia preferenziale e non sono mai soggette a questo limite."/>
|
||||||
<TextBox Grid.Row="0" Grid.Column="1" x:Name="PollIntervalFarMsTextBox" Text="2000" Margin="10,10"/>
|
<TextBox Grid.Row="0" Grid.Column="1" x:Name="MaxRequestsPerSecondTextBox" Text="0" Margin="10,10"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Polling 10-60s (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Timer di sistema a 1 ms" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Senza, ogni attesa può sforare di ~15 ms e l'anticipo perde significato"/>
|
||||||
<TextBox Grid.Row="1" Grid.Column="1" x:Name="PollIntervalMidMsTextBox" Text="900" Margin="10,10"/>
|
<CheckBox Grid.Row="1" Grid.Column="1" x:Name="PrecisionTimerCheckBox" Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Polling sotto 10s (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Grid.Row="2" Grid.Column="1" x:Name="PollIntervalNearMsTextBox" Text="400" Margin="10,10"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Polling in finestra critica (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Usato solo dalle aste in stato Attiva: in Osserva non c'è puntata da azzeccare"/>
|
|
||||||
<TextBox Grid.Row="3" Grid.Column="1" x:Name="PollIntervalCriticalMsTextBox" Text="220" Margin="10,10"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Ampiezza finestra critica (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Grid.Row="4" Grid.Column="1" x:Name="CriticalWindowMsTextBox" Text="4000" Margin="10,10"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="5" Grid.Column="0" Text="Limite richieste al secondo" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Le puntate hanno corsia preferenziale e non sono soggette a questo limite"/>
|
|
||||||
<TextBox Grid.Row="5" Grid.Column="1" x:Name="MaxRequestsPerSecondTextBox" Text="40" Margin="10,10"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="6" Grid.Column="0" Text="Timer di sistema a 1 ms" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Senza, ogni attesa può sforare di ~15 ms e l'anticipo perde significato"/>
|
|
||||||
<CheckBox Grid.Row="6" Grid.Column="1" x:Name="PrecisionTimerCheckBox" Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Border Style="{StaticResource InfoBox}" Margin="0,15,0,0">
|
<Border Style="{StaticResource InfoBox}" Margin="0,15,0,0">
|
||||||
@@ -405,7 +397,6 @@ Dalla rigiocata di 2.095 aste concluse: con anticipo sotto il secondo serve in m
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Misura l'anticipo effettivo"
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Misura l'anticipo effettivo"
|
||||||
@@ -413,15 +404,9 @@ Dalla rigiocata di 2.095 aste concluse: con anticipo sotto il secondo serve in m
|
|||||||
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="BidLeadTrackingCheckBox"
|
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="BidLeadTrackingCheckBox"
|
||||||
Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Proponi correzioni"
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Puntate minime per un consiglio"
|
||||||
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
|
||||||
ToolTip="Il consiglio non viene mai applicato da solo"/>
|
|
||||||
<CheckBox Grid.Row="1" Grid.Column="1" x:Name="BidLeadSuggestionsCheckBox"
|
|
||||||
Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Puntate minime per un consiglio"
|
|
||||||
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
<TextBox Grid.Row="2" Grid.Column="1" x:Name="BidLeadMinSamplesTextBox"
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="BidLeadMinSamplesTextBox"
|
||||||
Text="15" Margin="10,10"/>
|
Text="15" Margin="10,10"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -577,9 +562,6 @@ Dalla rigiocata di 2.095 aste concluse: con anticipo sotto il secondo serve in m
|
|||||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<CheckBox x:Name="DetailedStatsCheckBox" IsChecked="True" Margin="0,0,0,6"
|
|
||||||
Content="Registra una scheda dettagliata per ogni asta conclusa (serie prezzi, puntate per utente, durata)"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Carica impostazioni predefinite aste
|
// Carica impostazioni predefinite aste
|
||||||
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
|
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
|
||||||
|
Settings.AdaptiveLeadCheckBox.IsChecked = settings.AdaptiveLeadEnabled;
|
||||||
|
Settings.LeadMinMsTextBox.Text = settings.LeadMinMs.ToString();
|
||||||
|
Settings.LeadMaxMsTextBox.Text = settings.LeadMaxMs.ToString();
|
||||||
DefaultMinPrice.Text = settings.DefaultMinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
DefaultMinPrice.Text = settings.DefaultMinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
DefaultMaxPrice.Text = settings.DefaultMaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
DefaultMaxPrice.Text = settings.DefaultMaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
DefaultMaxClicks.Text = settings.DefaultMaxClicks.ToString();
|
DefaultMaxClicks.Text = settings.DefaultMaxClicks.ToString();
|
||||||
@@ -63,11 +66,6 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Motore di precisione
|
// Motore di precisione
|
||||||
Settings.PollIntervalFarMsTextBox.Text = settings.PollIntervalFarMs.ToString();
|
|
||||||
Settings.PollIntervalMidMsTextBox.Text = settings.PollIntervalMidMs.ToString();
|
|
||||||
Settings.PollIntervalNearMsTextBox.Text = settings.PollIntervalNearMs.ToString();
|
|
||||||
Settings.PollIntervalCriticalMsTextBox.Text = settings.PollIntervalCriticalMs.ToString();
|
|
||||||
Settings.CriticalWindowMsTextBox.Text = settings.CriticalWindowMs.ToString();
|
|
||||||
Settings.MaxRequestsPerSecondTextBox.Text = settings.MaxRequestsPerSecond.ToString("F0", System.Globalization.CultureInfo.InvariantCulture);
|
Settings.MaxRequestsPerSecondTextBox.Text = settings.MaxRequestsPerSecond.ToString("F0", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
Settings.PrecisionTimerCheckBox.IsChecked = settings.PrecisionTimerEnabled;
|
Settings.PrecisionTimerCheckBox.IsChecked = settings.PrecisionTimerEnabled;
|
||||||
|
|
||||||
@@ -88,7 +86,6 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Anticipo, aste programmate, notifiche, cartelle
|
// Anticipo, aste programmate, notifiche, cartelle
|
||||||
Settings.BidLeadTrackingCheckBox.IsChecked = settings.BidLeadTrackingEnabled;
|
Settings.BidLeadTrackingCheckBox.IsChecked = settings.BidLeadTrackingEnabled;
|
||||||
Settings.BidLeadSuggestionsCheckBox.IsChecked = settings.BidLeadSuggestionsEnabled;
|
|
||||||
Settings.BidLeadMinSamplesTextBox.Text = settings.BidLeadMinSamples.ToString();
|
Settings.BidLeadMinSamplesTextBox.Text = settings.BidLeadMinSamples.ToString();
|
||||||
|
|
||||||
Settings.ScheduledBackoffCheckBox.IsChecked = settings.ScheduledAuctionBackoffEnabled;
|
Settings.ScheduledBackoffCheckBox.IsChecked = settings.ScheduledAuctionBackoffEnabled;
|
||||||
@@ -104,7 +101,6 @@ namespace AutoBidder
|
|||||||
// ed è quello che serve sapere.
|
// ed è quello che serve sapere.
|
||||||
RefreshDataFolderFields();
|
RefreshDataFolderFields();
|
||||||
|
|
||||||
Settings.DetailedStatsCheckBox.IsChecked = settings.DetailedStatsEnabled;
|
|
||||||
Settings.CatalogCacheSecondsTextBox.Text = settings.CatalogCacheSeconds.ToString();
|
Settings.CatalogCacheSecondsTextBox.Text = settings.CatalogCacheSeconds.ToString();
|
||||||
|
|
||||||
Settings.QuietHoursCheckBox.IsChecked = settings.QuietHoursEnabled;
|
Settings.QuietHoursCheckBox.IsChecked = settings.QuietHoursEnabled;
|
||||||
@@ -190,6 +186,16 @@ namespace AutoBidder
|
|||||||
Log("[ERRORE] Valore anticipo puntata non valido (deve essere 0-5000ms)", LogLevel.Error);
|
Log("[ERRORE] Valore anticipo puntata non valido (deve essere 0-5000ms)", LogLevel.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paletti dell'anticipo adattivo: minimo sotto il massimo, entrambi sensati.
|
||||||
|
settings.AdaptiveLeadEnabled = Settings.AdaptiveLeadCheckBox.IsChecked == true;
|
||||||
|
settings.LeadMinMs = ReadBounded(Settings.LeadMinMsTextBox.Text, settings.LeadMinMs, 100, 5000, "anticipo minimo", " ms");
|
||||||
|
settings.LeadMaxMs = ReadBounded(Settings.LeadMaxMsTextBox.Text, settings.LeadMaxMs, 100, 5000, "anticipo massimo", " ms");
|
||||||
|
if (settings.LeadMaxMs < settings.LeadMinMs)
|
||||||
|
{
|
||||||
|
Log($"[ERRORE] Anticipo massimo ({settings.LeadMaxMs} ms) sotto il minimo ({settings.LeadMinMs} ms): riportato al minimo", LogLevel.Error);
|
||||||
|
settings.LeadMaxMs = settings.LeadMinMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (double.TryParse(DefaultMinPrice.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any,
|
if (double.TryParse(DefaultMinPrice.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any,
|
||||||
@@ -320,11 +326,6 @@ namespace AutoBidder
|
|||||||
// === SEZIONE: Motore di precisione ===
|
// === SEZIONE: Motore di precisione ===
|
||||||
// Ogni cadenza ha un minimo sensato: sotto i 100 ms si spreca banda senza
|
// Ogni cadenza ha un minimo sensato: sotto i 100 ms si spreca banda senza
|
||||||
// guadagnare precisione, perché la puntata la decide il cecchino, non il poll.
|
// guadagnare precisione, perché la puntata la decide il cecchino, non il poll.
|
||||||
settings.PollIntervalFarMs = ReadBounded(Settings.PollIntervalFarMsTextBox.Text, settings.PollIntervalFarMs, 200, 30000, "polling lontano");
|
|
||||||
settings.PollIntervalMidMs = ReadBounded(Settings.PollIntervalMidMsTextBox.Text, settings.PollIntervalMidMs, 150, 10000, "polling medio");
|
|
||||||
settings.PollIntervalNearMs = ReadBounded(Settings.PollIntervalNearMsTextBox.Text, settings.PollIntervalNearMs, 100, 5000, "polling vicino");
|
|
||||||
settings.PollIntervalCriticalMs = ReadBounded(Settings.PollIntervalCriticalMsTextBox.Text, settings.PollIntervalCriticalMs, 100, 3000, "polling critico");
|
|
||||||
settings.CriticalWindowMs = ReadBounded(Settings.CriticalWindowMsTextBox.Text, settings.CriticalWindowMs, 1000, 60000, "finestra critica");
|
|
||||||
|
|
||||||
if (double.TryParse(Settings.MaxRequestsPerSecondTextBox.Text.Replace(',', '.'),
|
if (double.TryParse(Settings.MaxRequestsPerSecondTextBox.Text.Replace(',', '.'),
|
||||||
System.Globalization.NumberStyles.Any,
|
System.Globalization.NumberStyles.Any,
|
||||||
@@ -392,7 +393,6 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// === SEZIONE: Anticipo puntata ===
|
// === SEZIONE: Anticipo puntata ===
|
||||||
settings.BidLeadTrackingEnabled = Settings.BidLeadTrackingCheckBox.IsChecked ?? true;
|
settings.BidLeadTrackingEnabled = Settings.BidLeadTrackingCheckBox.IsChecked ?? true;
|
||||||
settings.BidLeadSuggestionsEnabled = Settings.BidLeadSuggestionsCheckBox.IsChecked ?? true;
|
|
||||||
settings.BidLeadMinSamples = ReadBounded(Settings.BidLeadMinSamplesTextBox.Text,
|
settings.BidLeadMinSamples = ReadBounded(Settings.BidLeadMinSamplesTextBox.Text,
|
||||||
settings.BidLeadMinSamples, 5, 500, "puntate minime per il consiglio", "");
|
settings.BidLeadMinSamples, 5, 500, "puntate minime per il consiglio", "");
|
||||||
|
|
||||||
@@ -420,7 +420,6 @@ namespace AutoBidder
|
|||||||
settings.StatsFolder = NormalizeFolderChoice(Settings.StatsFolderTextBox.Text, AppPaths.StatsFolder, settings.StatsFolder);
|
settings.StatsFolder = NormalizeFolderChoice(Settings.StatsFolderTextBox.Text, AppPaths.StatsFolder, settings.StatsFolder);
|
||||||
settings.LogFolder = NormalizeFolderChoice(Settings.LogFolderTextBox.Text, AppPaths.LogFolder, settings.LogFolder);
|
settings.LogFolder = NormalizeFolderChoice(Settings.LogFolderTextBox.Text, AppPaths.LogFolder, settings.LogFolder);
|
||||||
|
|
||||||
settings.DetailedStatsEnabled = Settings.DetailedStatsCheckBox.IsChecked ?? true;
|
|
||||||
|
|
||||||
settings.QuietHoursEnabled = Settings.QuietHoursCheckBox.IsChecked ?? true;
|
settings.QuietHoursEnabled = Settings.QuietHoursCheckBox.IsChecked ?? true;
|
||||||
if (int.TryParse(Settings.QuietHoursStartTextBox.Text?.Trim(), out var qStart) && qStart is >= 0 and <= 23)
|
if (int.TryParse(Settings.QuietHoursStartTextBox.Text?.Trim(), out var qStart) && qStart is >= 0 and <= 23)
|
||||||
|
|||||||
@@ -68,13 +68,10 @@ namespace AutoBidder
|
|||||||
$"(prezzo {record.FinalPrice:F2} EUR)", LogLevel.Info);
|
$"(prezzo {record.FinalPrice:F2} EUR)", LogLevel.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// La scheda dettagliata vive solo nel riepilogo del dossier: l'archivio
|
||||||
|
// mensile che la duplicava non c'è più (vedi AuctionDetailStore).
|
||||||
var detail = BuildDetail(auction, state, record);
|
var detail = BuildDetail(auction, state, record);
|
||||||
|
|
||||||
if (settings.DetailedStatsEnabled)
|
|
||||||
{
|
|
||||||
AuctionDetailStore.Append(detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Il dossier si chiude con il riepilogo: da lì in poi quel file è una
|
// Il dossier si chiude con il riepilogo: da lì in poi quel file è una
|
||||||
// storia completa, ed è così che l'analisi sa di poterlo usare. Il percorso
|
// storia completa, ed è così che l'analisi sa di poterlo usare. Il percorso
|
||||||
// si prende prima, perché Close lo toglie dal registro dei dossier aperti.
|
// si prende prima, perché Close lo toglie dal registro dei dossier aperti.
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ namespace AutoBidder
|
|||||||
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedBidBeforeDeadlineMs.Text, out int ms))
|
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedBidBeforeDeadlineMs.Text, out int ms))
|
||||||
{
|
{
|
||||||
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = ms;
|
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = ms;
|
||||||
|
_selectedAuction.AuctionInfo.BidLeadIsManual = true;
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,25 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Export_ClearExportsClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
"Cancello tutti i file nella cartella delle esportazioni? Sono copie: si rigenerano in qualunque momento.",
|
||||||
|
"Svuota le esportazioni", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (files, bytes) = StatsWipe.ClearExports();
|
||||||
|
Log($"[ESPORTA] Cartella svuotata: {files} file, {bytes / 1024.0:F0} KB liberati", LogLevel.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[ESPORTA] Svuotamento non riuscito: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Export_OpenLastFileClicked(object sender, RoutedEventArgs e)
|
private void Export_OpenLastFileClicked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var path = Export.LastExportPath;
|
var path = Export.LastExportPath;
|
||||||
|
|||||||
@@ -69,21 +69,22 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Svuota lo storico delle aste concluse (con conferma).</summary>
|
/// <summary>
|
||||||
|
/// Azzera le statistiche registrate: il dialogo dice cosa c'è e quanto pesa, e
|
||||||
|
/// l'utente sceglie voce per voce. Vedi <see cref="StatsWipe"/>.
|
||||||
|
/// </summary>
|
||||||
private void ClearStatsButton_Click(object sender, RoutedEventArgs e)
|
private void ClearStatsButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var res = MessageBox.Show(this,
|
var dialog = new Dialogs.WipeStatsDialog { Owner = this };
|
||||||
"Vuoi eliminare tutto lo storico delle aste concluse? L'operazione non è reversibile.",
|
if (dialog.ShowDialog() != true || dialog.Result is not { } report) return;
|
||||||
"Svuota storico",
|
|
||||||
MessageBoxButton.YesNo,
|
|
||||||
MessageBoxImage.Warning);
|
|
||||||
|
|
||||||
if (res == MessageBoxResult.Yes)
|
LoadStatistics();
|
||||||
{
|
LoadProducts();
|
||||||
CompletedAuctionsStore.Clear();
|
if (Learning.IsVisible) Learning.Refresh();
|
||||||
LoadStatistics();
|
|
||||||
Log("[STATISTICHE] Storico svuotato", LogLevel.Info);
|
Log($"[STATISTICHE] Azzeramento: {report.Summary}. " +
|
||||||
}
|
$"{report.FilesDeleted} file tolti, {report.BytesFreed / (1024.0 * 1024.0):F1} MB liberati",
|
||||||
|
report.Errors.Count == 0 ? LogLevel.Success : LogLevel.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Resetta ai valori predefiniti dalle impostazioni
|
// Resetta ai valori predefiniti dalle impostazioni
|
||||||
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
|
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
_selectedAuction.AuctionInfo.BidLeadIsManual = false; // torna all'anticipo adattivo
|
||||||
_selectedAuction.MinPrice = settings.DefaultMinPrice;
|
_selectedAuction.MinPrice = settings.DefaultMinPrice;
|
||||||
_selectedAuction.MaxPrice = settings.DefaultMaxPrice;
|
_selectedAuction.MaxPrice = settings.DefaultMaxPrice;
|
||||||
_selectedAuction.MaxClicks = settings.DefaultMaxClicks;
|
_selectedAuction.MaxClicks = settings.DefaultMaxClicks;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<Window x:Class="AutoBidder.Dialogs.WipeStatsDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Azzera le statistiche"
|
||||||
|
Width="600" SizeToContent="Height"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
Background="{DynamicResource Brush.Bg}">
|
||||||
|
|
||||||
|
<StackPanel Margin="22,18">
|
||||||
|
|
||||||
|
<TextBlock Text="Cosa azzerare"
|
||||||
|
FontSize="{StaticResource Font.Size.Lg}" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource Hint}" Margin="0,6,0,14"
|
||||||
|
Text="Accanto a ogni voce c'è quanto spazio occupa adesso. Niente viene toccato finché non premi Azzera. Lo storico delle aste concluse viene copiato nella cartella dei backup prima di sparire; il resto no."/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkHistory" Margin="0,4" IsChecked="True"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
ToolTip="Il file completed-auctions.json: una riga per asta conclusa. È la base di tutto il resto — griglia Storico, statistiche per prodotto, limiti consigliati. Viene copiato nei backup prima di essere cancellato."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkProducts" Margin="0,4" IsChecked="True"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
ToolTip="I totali per prodotto (aste viste, prezzi medi, puntate del vincitore). Le opzioni scelte nella scheda Prodotti restano: si azzerano i numeri, non le scelte."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkBidLead" Margin="0,4" IsChecked="True"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
ToolTip="Le misure dell'anticipo effettivo di ogni puntata (bid-lead-stats.json)."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkExports" Margin="0,4" IsChecked="True"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
ToolTip="Tutti i file nella cartella Esportazioni. Sono copie: si rigenerano in qualunque momento dalla scheda Esporta."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkLegacy" Margin="0,4" IsChecked="True"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
ToolTip="Gli archivi mensili aste-AAAA-MM.jsonl delle versioni precedenti: erano una copia del riepilogo che ogni dossier ha già in coda, e l'applicazione non li scrive né li legge più."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkLearning" Margin="0,4" IsChecked="False"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
ToolTip="Il modello appreso, il profilo per prodotto e ora, il modello di latenza e l'elenco dei dossier già studiati. Non si perde niente di irrecuperabile: al prossimo avvio l'applicazione ristudia tutti i dossier da capo (qualche minuto in sottofondo)."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource InfoBox}" Margin="0,14,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="SummaryText" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}" LineHeight="18"
|
||||||
|
Text="Calcolo…"/>
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin="0,8,0,0"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}" LineHeight="17"
|
||||||
|
Text="I dossier delle aste (cartella Registri\Aste) non vengono toccati da qui: sono i dati grezzi da cui tutto il resto si ricostruisce. Per liberare quello spazio usa la scheda Dati."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
|
||||||
|
<Button x:Name="WipeButton" Content="Azzera" Padding="26,7" Margin="0,0,10,0"
|
||||||
|
Style="{StaticResource ModernButton}"
|
||||||
|
Background="{DynamicResource Brush.Danger}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Click="Wipe_Click"/>
|
||||||
|
<Button Content="Annulla" Padding="20,7" IsCancel="True"
|
||||||
|
Style="{StaticResource ModernButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sceglie quali statistiche azzerare e le azzera.
|
||||||
|
///
|
||||||
|
/// <para>Accanto a ogni voce c'è quanto pesa adesso: cancellare è irreversibile, e
|
||||||
|
/// l'unico modo di renderlo una decisione informata è dire cosa sparisce e quanto è.
|
||||||
|
/// Lo storico delle aste concluse è l'unica voce con copia di sicurezza automatica:
|
||||||
|
/// è il solo archivio che non si ricostruisce da nient'altro.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class WipeStatsDialog : Window
|
||||||
|
{
|
||||||
|
private StatsWipe.Sizes _sizes;
|
||||||
|
|
||||||
|
public WipeStatsDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += (_, _) => { _sizes = StatsWipe.Measure(); Refresh(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Il rapporto dell'azzeramento, dopo la conferma. Null se annullato.</summary>
|
||||||
|
public StatsWipe.Report? Result { get; private set; }
|
||||||
|
|
||||||
|
private StatsWipe.Options ReadOptions() => new()
|
||||||
|
{
|
||||||
|
History = ChkHistory.IsChecked == true,
|
||||||
|
ProductStats = ChkProducts.IsChecked == true,
|
||||||
|
BidLeadMeasures = ChkBidLead.IsChecked == true,
|
||||||
|
Exports = ChkExports.IsChecked == true,
|
||||||
|
LegacyArchives = ChkLegacy.IsChecked == true,
|
||||||
|
Learning = ChkLearning.IsChecked == true
|
||||||
|
};
|
||||||
|
|
||||||
|
private void Option_Changed(object sender, RoutedEventArgs e) => Refresh();
|
||||||
|
|
||||||
|
private void Refresh()
|
||||||
|
{
|
||||||
|
if (!IsLoaded) return;
|
||||||
|
|
||||||
|
ChkHistory.Content = $"Storico delle aste concluse ({_sizes.HistoryAuctions} aste, {Fmt(_sizes.HistoryBytes)})";
|
||||||
|
ChkProducts.Content = $"Statistiche per prodotto ({Fmt(_sizes.ProductsBytes)})";
|
||||||
|
ChkBidLead.Content = $"Misure dell'anticipo effettivo ({_sizes.BidLeadSamples} misure, {Fmt(_sizes.BidLeadBytes)})";
|
||||||
|
ChkExports.Content = $"Esportazioni ({_sizes.ExportFiles} file, {Fmt(_sizes.ExportBytes)})";
|
||||||
|
ChkLegacy.Content = $"Archivi mensili delle versioni precedenti ({_sizes.LegacyFiles} file, {Fmt(_sizes.LegacyBytes)})";
|
||||||
|
ChkLearning.Content = $"Apprendimento: modello, profilo, latenza ({Fmt(_sizes.LearningBytes)})";
|
||||||
|
|
||||||
|
var o = ReadOptions();
|
||||||
|
long totale = 0;
|
||||||
|
if (o.History) totale += _sizes.HistoryBytes;
|
||||||
|
if (o.ProductStats) totale += _sizes.ProductsBytes;
|
||||||
|
if (o.BidLeadMeasures) totale += _sizes.BidLeadBytes;
|
||||||
|
if (o.Exports) totale += _sizes.ExportBytes;
|
||||||
|
if (o.LegacyArchives) totale += _sizes.LegacyBytes;
|
||||||
|
if (o.Learning) totale += _sizes.LearningBytes;
|
||||||
|
|
||||||
|
SummaryText.Text = o.Nothing
|
||||||
|
? "Nessuna voce scelta."
|
||||||
|
: $"Si liberano circa {Fmt(totale)}." +
|
||||||
|
(o.History ? " Lo storico viene prima copiato nei backup." : "") +
|
||||||
|
(o.Learning ? " L'apprendimento ripartirà da zero e ristudierà i dossier al prossimo avvio." : "");
|
||||||
|
|
||||||
|
WipeButton.IsEnabled = !o.Nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Wipe_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var o = ReadOptions();
|
||||||
|
if (o.Nothing) return;
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
"Le voci scelte vengono cancellate. L'operazione non è reversibile" +
|
||||||
|
(o.History ? ", salvo la copia dello storico nei backup" : "") + ".\n\nProcedo?",
|
||||||
|
"Azzera le statistiche", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Result = StatsWipe.Run(o);
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"Azzeramento non riuscito: {ex.Message}",
|
||||||
|
"Azzera le statistiche", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Fmt(long bytes) =>
|
||||||
|
bytes >= 1L << 30 ? $"{bytes / (double)(1L << 30):F2} GB"
|
||||||
|
: bytes >= 1L << 20 ? $"{bytes / (double)(1L << 20):F1} MB"
|
||||||
|
: bytes >= 1L << 10 ? $"{bytes / (double)(1L << 10):F0} KB"
|
||||||
|
: $"{bytes} B";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -228,6 +228,33 @@ namespace AutoBidder.Engine
|
|||||||
return (int)Math.Clamp(Math.Min(desired, untilWake), 1000, 1_800_000);
|
return (int)Math.Clamp(Math.Min(desired, untilWake), 1000, 1_800_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L'anticipo con cui parte la puntata su quest'asta.
|
||||||
|
///
|
||||||
|
/// <para>Un anticipo scritto a mano sull'asta vince sempre. Altrimenti decide il
|
||||||
|
/// modello di latenza — margine più coda del ping di questa sessione, dentro i
|
||||||
|
/// paletti delle impostazioni — e il valore scelto viene scritto sull'asta, così
|
||||||
|
/// la griglia e il dossier mostrano l'anticipo davvero in uso.</para>
|
||||||
|
/// </summary>
|
||||||
|
private int EffectiveLeadMs(AppSettings settings)
|
||||||
|
{
|
||||||
|
if (_auction.BidLeadIsManual && _auction.BidBeforeDeadlineMs > 0)
|
||||||
|
return _auction.BidBeforeDeadlineMs;
|
||||||
|
|
||||||
|
var lead = settings.AdaptiveLeadEnabled
|
||||||
|
? Ml.LatencyModel.RecommendedLeadMs(settings)
|
||||||
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
|
||||||
|
if (lead != _auction.BidBeforeDeadlineMs)
|
||||||
|
{
|
||||||
|
_auction.AddLog($"Anticipo adattivo: {lead} ms (era {_auction.BidBeforeDeadlineMs} ms)",
|
||||||
|
AuctionLogLevel.Debug, AuctionLogCategory.Ticker);
|
||||||
|
_auction.BidBeforeDeadlineMs = lead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lead;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Anello del cecchino ──────────────────────────────────────────────────
|
// ── Anello del cecchino ──────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task SniperLoopAsync(CancellationToken ct)
|
private async Task SniperLoopAsync(CancellationToken ct)
|
||||||
@@ -265,9 +292,7 @@ namespace AutoBidder.Engine
|
|||||||
}
|
}
|
||||||
|
|
||||||
var settings = _host.Settings;
|
var settings = _host.Settings;
|
||||||
var leadMs = _auction.BidBeforeDeadlineMs > 0
|
var leadMs = EffectiveLeadMs(settings);
|
||||||
? _auction.BidBeforeDeadlineMs
|
|
||||||
: settings.DefaultBidBeforeDeadlineMs;
|
|
||||||
var fireTicks = deadline - PrecisionWait.MsToTicks(leadMs);
|
var fireTicks = deadline - PrecisionWait.MsToTicks(leadMs);
|
||||||
|
|
||||||
var msToDeadline = PrecisionWait.MsUntil(deadline);
|
var msToDeadline = PrecisionWait.MsUntil(deadline);
|
||||||
|
|||||||
@@ -96,12 +96,11 @@ namespace AutoBidder.Engine.Backtest
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(reason)) return "altro";
|
if (string.IsNullOrEmpty(reason)) return "altro";
|
||||||
|
|
||||||
if (reason.StartsWith("Anti-bot", StringComparison.OrdinalIgnoreCase)) return "anti-bot";
|
if (reason.StartsWith("autopuntata avversaria", StringComparison.OrdinalIgnoreCase)) return "duello";
|
||||||
if (reason.StartsWith("Soft retreat", StringComparison.OrdinalIgnoreCase)) return "soft-retreat";
|
if (reason.StartsWith("puntata saltata: fascia", StringComparison.OrdinalIgnoreCase)) return "fascia-oraria";
|
||||||
if (reason.StartsWith("Asta troppo calda", StringComparison.OrdinalIgnoreCase)) return "asta-calda";
|
if (reason.StartsWith("valore atteso", StringComparison.OrdinalIgnoreCase)) return "valore-atteso";
|
||||||
if (reason.StartsWith("Bidder aggressivi", StringComparison.OrdinalIgnoreCase)) return "avversari-aggressivi";
|
if (reason.StartsWith("lascio sfogare", StringComparison.OrdinalIgnoreCase)) return "sfogo";
|
||||||
if (reason.StartsWith("Skip probabilistico", StringComparison.OrdinalIgnoreCase)) return "probabilistico";
|
if (reason.StartsWith("puntata di prova", StringComparison.OrdinalIgnoreCase)) return "sondaggio";
|
||||||
if (reason.StartsWith("Prezzo sale", StringComparison.OrdinalIgnoreCase)) return "velocita-prezzo";
|
|
||||||
if (reason.StartsWith("Limite puntate", StringComparison.OrdinalIgnoreCase)) return "limite-puntate";
|
if (reason.StartsWith("Limite puntate", StringComparison.OrdinalIgnoreCase)) return "limite-puntate";
|
||||||
if (reason.StartsWith("Budget", StringComparison.OrdinalIgnoreCase)) return "budget";
|
if (reason.StartsWith("Budget", StringComparison.OrdinalIgnoreCase)) return "budget";
|
||||||
|
|
||||||
@@ -181,7 +180,6 @@ namespace AutoBidder.Engine.Backtest
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
strategy.UpdateHeatMetric(auction, options.EffectiveSettings, options.Username);
|
|
||||||
var decision = strategy.ShouldPlaceBid(auction, state, options.EffectiveSettings, options.Username);
|
var decision = strategy.ShouldPlaceBid(auction, state, options.EffectiveSettings, options.Username);
|
||||||
|
|
||||||
if (!decision.ShouldBid)
|
if (!decision.ShouldBid)
|
||||||
@@ -312,6 +310,7 @@ namespace AutoBidder.Engine.Backtest
|
|||||||
Name = header?.Name ?? "",
|
Name = header?.Name ?? "",
|
||||||
BuyNowPrice = header?.BuyNowPrice,
|
BuyNowPrice = header?.BuyNowPrice,
|
||||||
BidBeforeDeadlineMs = options.LeadMs,
|
BidBeforeDeadlineMs = options.LeadMs,
|
||||||
|
BidLeadIsManual = true, // la rigiocata prova un anticipo preciso, non quello adattivo
|
||||||
BidsUsedOnThisAuction = 0
|
BidsUsedOnThisAuction = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,8 @@
|
|||||||
Visibility="Collapsed"
|
Visibility="Collapsed"
|
||||||
ExportRequested="Export_ExportRequested"
|
ExportRequested="Export_ExportRequested"
|
||||||
OpenExportFolderClicked="Export_OpenFolderClicked"
|
OpenExportFolderClicked="Export_OpenFolderClicked"
|
||||||
OpenLastFileClicked="Export_OpenLastFileClicked"/>
|
OpenLastFileClicked="Export_OpenLastFileClicked"
|
||||||
|
ClearExportsClicked="Export_ClearExportsClicked"/>
|
||||||
|
|
||||||
<!-- Apprendimento Panel -->
|
<!-- Apprendimento Panel -->
|
||||||
<controls:LearningControl x:Name="Learning" Visibility="Collapsed"/>
|
<controls:LearningControl x:Name="Learning" Visibility="Collapsed"/>
|
||||||
@@ -300,7 +301,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button Style="{StaticResource IconButton}"
|
<Button Style="{StaticResource IconButton}"
|
||||||
Foreground="{DynamicResource Brush.Danger}"
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
ToolTip="Svuota lo storico" Click="ClearStatsButton_Click">
|
ToolTip="Azzera le statistiche registrate: scegli cosa (storico, prodotti, misure, esportazioni, archivi vecchi, apprendimento). Lo storico viene prima copiato nei backup."
|
||||||
|
Click="ClearStatsButton_Click">
|
||||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Inizializza servizi
|
// Inizializza servizi
|
||||||
_auctionMonitor = new AuctionMonitor();
|
_auctionMonitor = new AuctionMonitor();
|
||||||
|
Learning.AuctionsProvider = () => _auctionMonitor.GetAuctions();
|
||||||
|
|
||||||
// ? NUOVO: Inizializza SessionService
|
// ? NUOVO: Inizializza SessionService
|
||||||
InitializeSessionService();
|
InitializeSessionService();
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AutoBidder.Ml
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Il regime di concorrenza di un'asta: quando lasciar sfogare gli altri, e quando
|
||||||
|
/// tornare a puntare. Sostituisce «calore», «ritiro morbido», «avversari aggressivi» e
|
||||||
|
/// il vecchio rilevamento del duello con una sola macchina a stati, guidata da ciò
|
||||||
|
/// che il modello ha imparato invece che da soglie scritte a mano.
|
||||||
|
///
|
||||||
|
/// <para><b>Gli stati.</b></para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>Calmo</b>: si punta quando il valore atteso è positivo. È il regime
|
||||||
|
/// normale.</item>
|
||||||
|
/// <item><b>Sfogo</b>: gli altri si stanno battendo e ogni nostra puntata verrebbe
|
||||||
|
/// coperta. Non si punta. Ci si entra quando il valore atteso resta negativo per
|
||||||
|
/// qualche decisione di fila, o quando si riconosce un'autopuntata armata.</item>
|
||||||
|
/// <item><b>Sondaggio</b>: le condizioni sono tornate buone da abbastanza cicli. Si
|
||||||
|
/// concede <i>una</i> puntata di prova. Se resta senza risposta si torna Calmo; se
|
||||||
|
/// viene coperta subito si torna a Sfogo, e la pazienza richiesta per il prossimo
|
||||||
|
/// sondaggio raddoppia.</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// <para><b>Perché l'isteresi.</b> Il valore atteso oscilla da un ciclo all'altro: un
|
||||||
|
/// cancello che punta al primo segno positivo e si ferma al primo negativo farebbe
|
||||||
|
/// avanti e indietro, e ogni «avanti» è una puntata pagata. Servono più segnali
|
||||||
|
/// buoni di fila per rientrare che segnali cattivi per uscire: sbagliare a rientrare
|
||||||
|
/// costa soldi, sbagliare a uscire costa al massimo un'attesa.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Perché la pazienza raddoppia.</b> Un sondaggio coperto subito dice che
|
||||||
|
/// dall'altra parte c'è ancora qualcuno che risponde: insistere con la stessa
|
||||||
|
/// pazienza sarebbe rifare lo stesso errore allo stesso prezzo. Raddoppiare fino a un
|
||||||
|
/// tetto è il modo più semplice di «capire da soli» che quell'asta, oggi, non si
|
||||||
|
/// lascia prendere — senza deciderlo una volta per tutte.</para>
|
||||||
|
///
|
||||||
|
/// <para>Classe pura con stato proprio: riceve i segnali, non legge niente da fuori.
|
||||||
|
/// Così si prova a tavolino e si rigioca sui dossier con la stessa logica del motore.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CompetitionRegime
|
||||||
|
{
|
||||||
|
public enum Stato { Calmo, Sfogo, Sondaggio }
|
||||||
|
|
||||||
|
/// <summary>Decisioni negative di fila prima di andare in Sfogo.</summary>
|
||||||
|
public const int NegativiPerSfogo = 3;
|
||||||
|
|
||||||
|
/// <summary>Cicli buoni di fila richiesti al primo rientro; raddoppia a ogni sondaggio coperto.</summary>
|
||||||
|
public const int PazienzaIniziale = 3;
|
||||||
|
|
||||||
|
public const int PazienzaMassima = 24;
|
||||||
|
|
||||||
|
/// <summary>Secondi entro cui una risposta alla puntata di prova conta come «coperta subito».</summary>
|
||||||
|
public const double RispostaRapidaSecondi = 9.0;
|
||||||
|
|
||||||
|
public Stato StatoAttuale { get; private set; } = Stato.Calmo;
|
||||||
|
|
||||||
|
/// <summary>Quanti cicli buoni di fila servono adesso per rientrare.</summary>
|
||||||
|
public int Pazienza { get; private set; } = PazienzaIniziale;
|
||||||
|
|
||||||
|
/// <summary>Cicli buoni di fila visti finora durante lo Sfogo.</summary>
|
||||||
|
public int CicliBuoni { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Decisioni negative di fila viste finora in Calmo.</summary>
|
||||||
|
public int Negativi { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Sondaggi coperti subito, in quest'asta.</summary>
|
||||||
|
public int SondaggiFalliti { get; private set; }
|
||||||
|
|
||||||
|
private bool _sondaggioInCorso;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Una decisione del motore: il valore atteso di puntare adesso era positivo?
|
||||||
|
/// Restituisce true se in questo regime si può puntare.
|
||||||
|
/// </summary>
|
||||||
|
public bool Osserva(bool valoreAttesoPositivo)
|
||||||
|
{
|
||||||
|
switch (StatoAttuale)
|
||||||
|
{
|
||||||
|
case Stato.Calmo:
|
||||||
|
if (valoreAttesoPositivo) { Negativi = 0; return true; }
|
||||||
|
if (++Negativi >= NegativiPerSfogo) EntraInSfogo();
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case Stato.Sfogo:
|
||||||
|
if (!valoreAttesoPositivo) { CicliBuoni = 0; return false; }
|
||||||
|
if (++CicliBuoni < Pazienza) return false;
|
||||||
|
// Abbastanza cicli buoni di fila: una puntata di prova.
|
||||||
|
StatoAttuale = Stato.Sondaggio;
|
||||||
|
_sondaggioInCorso = false;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Stato.Sondaggio:
|
||||||
|
// Finché la prova non è stata piazzata si può ancora puntare (una volta);
|
||||||
|
// dopo, si aspetta l'esito.
|
||||||
|
return !_sondaggioInCorso && valoreAttesoPositivo;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>La nostra puntata è partita.</summary>
|
||||||
|
public void PuntataPiazzata()
|
||||||
|
{
|
||||||
|
if (StatoAttuale == Stato.Sondaggio) _sondaggioInCorso = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Qualcuno ha risposto alla nostra ultima puntata dopo tanti secondi. Se eravamo
|
||||||
|
/// in Sondaggio e la risposta è rapida, il sondaggio è fallito.
|
||||||
|
/// </summary>
|
||||||
|
public void RispostaAvversaria(double secondiDopoLaMiaPuntata)
|
||||||
|
{
|
||||||
|
if (StatoAttuale != Stato.Sondaggio || !_sondaggioInCorso) return;
|
||||||
|
|
||||||
|
if (secondiDopoLaMiaPuntata <= RispostaRapidaSecondi)
|
||||||
|
{
|
||||||
|
SondaggiFalliti++;
|
||||||
|
Pazienza = Math.Min(PazienzaMassima, Pazienza * 2);
|
||||||
|
EntraInSfogo();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Risposta lenta: non era una macchina, si torna a giocare.
|
||||||
|
Riprendi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>La nostra ultima puntata è rimasta senza risposta: siamo in testa da un pezzo.</summary>
|
||||||
|
public void NessunaRisposta()
|
||||||
|
{
|
||||||
|
if (StatoAttuale == Stato.Sondaggio) Riprendi();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Riconosciuta un'autopuntata armata: non c'è niente da sondare per ora.</summary>
|
||||||
|
public void AutopuntataRiconosciuta()
|
||||||
|
{
|
||||||
|
if (StatoAttuale != Stato.Sfogo)
|
||||||
|
{
|
||||||
|
Pazienza = Math.Max(Pazienza, PazienzaIniziale * 2);
|
||||||
|
EntraInSfogo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EntraInSfogo()
|
||||||
|
{
|
||||||
|
StatoAttuale = Stato.Sfogo;
|
||||||
|
CicliBuoni = 0;
|
||||||
|
Negativi = 0;
|
||||||
|
_sondaggioInCorso = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Riprendi()
|
||||||
|
{
|
||||||
|
StatoAttuale = Stato.Calmo;
|
||||||
|
Negativi = 0;
|
||||||
|
CicliBuoni = 0;
|
||||||
|
_sondaggioInCorso = false;
|
||||||
|
// La pazienza non si azzera del tutto: chi ci ha coperto una volta può tornare.
|
||||||
|
Pazienza = Math.Max(PazienzaIniziale, Pazienza / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Una riga per il registro.</summary>
|
||||||
|
public string Spiegazione() => StatoAttuale switch
|
||||||
|
{
|
||||||
|
Stato.Sfogo => $"lascio sfogare gli altri: rientro dopo {Pazienza} cicli buoni di fila (visti {CicliBuoni})",
|
||||||
|
Stato.Sondaggio => "puntata di prova: se viene coperta subito torno ad aspettare",
|
||||||
|
_ => "regime calmo"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# Come decide il motore
|
||||||
|
|
||||||
|
Questo documento descrive l'algoritmo di puntata di AutoBidder dopo la rimozione delle
|
||||||
|
euristiche a soglia fissa. Vale come specifica: il codice in `Services/BidStrategyService.cs`,
|
||||||
|
`Engine/AuctionRunner.cs` e in questa cartella la implementa, e i test in `Tests/` la
|
||||||
|
verificano.
|
||||||
|
|
||||||
|
## Principio
|
||||||
|
|
||||||
|
**Tutto ciò che si può imparare dai dati non si scrive a mano.** L'utente fissa i
|
||||||
|
paletti — quanto è disposto a spendere, quando non vuole puntare, entro quali limiti
|
||||||
|
l'anticipo può muoversi — e dentro quei paletti il sistema decide da solo, imparando
|
||||||
|
sia dallo storico (seimila dossier) sia dalla sessione in corso (il ping di adesso, le
|
||||||
|
risposte degli avversari di adesso).
|
||||||
|
|
||||||
|
Le vecchie euristiche — calore dell'asta, ritiro morbido dopo le collisioni, avversari
|
||||||
|
aggressivi, anti-bot, puntata probabilistica, velocità del prezzo, esaurimento
|
||||||
|
dell'avversario — sono state tolte perché rigiocate sui dossier non hanno mai fermato
|
||||||
|
una puntata sbagliata senza fermarne anche di giuste, e i loro numeri magici andavano
|
||||||
|
tarati a mano per ogni prodotto e ora del giorno.
|
||||||
|
|
||||||
|
## La pipeline: quattro passi, sempre nello stesso ordine
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ 1. PALETTI │──▶│ 2. DUELLO │──▶│ 3. VALORE │──▶│ 4. REGIME │──▶ punta?
|
||||||
|
│ (utente) │ │ (sessione) │ │ ATTESO │ │ (sessione) │
|
||||||
|
└──────────────┘ └──────────────┘ │ (storico+ora)│ └──────────────┘
|
||||||
|
└──────────────┘
|
||||||
|
│
|
||||||
|
┌──────────▼──────────┐
|
||||||
|
│ QUANDO: anticipo │
|
||||||
|
│ adattivo (sessione) │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Paletti — `BidStrategyService`, `BiddingHours`
|
||||||
|
|
||||||
|
Regole dell'utente che non si discutono e non imparano niente:
|
||||||
|
|
||||||
|
- tetti di puntate per asta e per sessione, budget giornaliero (`BankrollManager*`);
|
||||||
|
- fascia oraria sospesa (`QuietHours*`): fra le 0 e le 9 la stessa asta costa quasi il
|
||||||
|
doppio che fra le 10 e le 13 (6060 aste concluse). L'asta resta *Attiva* e riprende da
|
||||||
|
sola alla fine della fascia; si può scavalcare per prodotto e per singola asta.
|
||||||
|
|
||||||
|
### 2. Duello con l'autopuntata — `AutoBidDuel`
|
||||||
|
|
||||||
|
L'autopuntata ufficiale di Bidoo scatta a 2 s dalla fine e risponde alla nostra puntata
|
||||||
|
in 6 ± 2,5 s, sempre. Cinque risposte di fila con quella firma = autopuntata armata. Nelle
|
||||||
|
prove reali il motore ci ha lasciato 191 puntate su tre aste per zero vittorie. Il
|
||||||
|
riconoscimento è un segnale della sessione in corso: informa il regime (passo 4) e, se
|
||||||
|
l'utente lo vuole (`AutoBidDuelWithdrawEnabled`, predefinito acceso), ferma le puntate.
|
||||||
|
|
||||||
|
### 3. Valore atteso — `LearningService`, `OnlineLogit`, `BidFeatures`
|
||||||
|
|
||||||
|
Un modello logistico in linea, aggiornato a ogni asta chiusa, stima la probabilità `p`
|
||||||
|
che una puntata fatta *adesso* resti senza risposta. Le variabili sono a fasce e leggibili:
|
||||||
|
prezzo in percentuale del valore, ora e giorno, prodotto (32 cassetti con hash stabile),
|
||||||
|
densità delle puntate negli ultimi cicli, profondità del ciclo, se l'ultimo puntatore
|
||||||
|
risponde a macchina.
|
||||||
|
|
||||||
|
Il valore atteso in euro di puntare è
|
||||||
|
|
||||||
|
```
|
||||||
|
EV = p × (Valore − Prezzo − 0,01) − CostoPuntata
|
||||||
|
```
|
||||||
|
|
||||||
|
Sotto zero, in media, puntare perde soldi. La calibrazione si corregge in linea con il
|
||||||
|
rapporto osservato/previsto (finestra 20 000, limitata a 0,25–4): il mercato deriva, e
|
||||||
|
un modello congelato sbaglierebbe di 1,5–1,8×. Il modello parla sempre; decide solo
|
||||||
|
dopo `LearningMinAuctions` aste apprese (predefinito 300).
|
||||||
|
|
||||||
|
Valutazione prequenziale sui dossier reali: sollevamento 10,8× sul decile alto,
|
||||||
|
calibrazione entro il 10–30%; 45 delle 116 puntate vere dell'utente sarebbero state
|
||||||
|
fermate senza perdere nessuna vittoria.
|
||||||
|
|
||||||
|
### 4. Regime di concorrenza — `CompetitionRegime`
|
||||||
|
|
||||||
|
Il valore atteso è un numero per istante; il regime lo trasforma in una condotta per
|
||||||
|
asta. Tre stati:
|
||||||
|
|
||||||
|
| Stato | Cosa fa | Ci si entra quando |
|
||||||
|
|---|---|---|
|
||||||
|
| **Calmo** | punta se EV ≥ 0 | stato iniziale; un sondaggio riuscito |
|
||||||
|
| **Sfogo** | non punta: lascia che gli altri si battano | 3 EV negativi di fila; autopuntata riconosciuta; sondaggio coperto subito |
|
||||||
|
| **Sondaggio** | una sola puntata di prova | in Sfogo, `Pazienza` cicli buoni di fila |
|
||||||
|
|
||||||
|
**Isteresi nel verso giusto.** Uscire è facile (3 negativi), rientrare è difficile
|
||||||
|
(`Pazienza` positivi di fila, che parte da 3). Sbagliare a rientrare costa una puntata;
|
||||||
|
sbagliare a uscire costa al massimo un'attesa.
|
||||||
|
|
||||||
|
**La pazienza raddoppia.** Se la puntata di prova viene coperta entro 9 s, il regime torna
|
||||||
|
in Sfogo e la pazienza raddoppia (fino a 24). Così il sistema "capisce da solo" che quell'asta
|
||||||
|
oggi non si lascia prendere, senza deciderlo una volta per tutte: se la risposta arriva
|
||||||
|
lenta (una persona, non una macchina) si torna Calmo e la pazienza si dimezza.
|
||||||
|
|
||||||
|
È questo il passo che risponde a «quando c'è troppa concorrenza li lascio sfogare, e il
|
||||||
|
sistema deve capire da solo quando può tornare a puntare».
|
||||||
|
|
||||||
|
### Quando puntare: l'anticipo adattivo — `LatencyModel`
|
||||||
|
|
||||||
|
L'anticipo giusto dipende dalla latenza di *questa* rete in *questo* momento. Un numero
|
||||||
|
fisso è sbagliato quasi sempre. La regola:
|
||||||
|
|
||||||
|
```
|
||||||
|
anticipo = clamp( margine + p99(latenza) , LeadMinMs , LeadMaxMs )
|
||||||
|
```
|
||||||
|
|
||||||
|
- la **coda** (p99 delle ultime 300 andate-e-ritorno delle puntate, o dei ping delle
|
||||||
|
interrogazioni finché non ci sono puntate), non la media: una puntata su cento in
|
||||||
|
ritardo costa un'asta intera;
|
||||||
|
- il **margine** (parte da 300 ms) sale di 150 ms a ogni puntata tardiva e scende di
|
||||||
|
25 ms ogni 20 puntate in tempo. Asimmetrico apposta;
|
||||||
|
- i **paletti** `LeadMinMs` (300) e `LeadMaxMs` (1500) li fissa l'utente; un anticipo
|
||||||
|
scritto a mano su una singola asta vince sempre (`BidLeadIsManual`).
|
||||||
|
|
||||||
|
Lo stato si salva fra le sessioni (`Statistiche/Apprendimento/latenza.json`).
|
||||||
|
|
||||||
|
## Cosa impara, e da dove
|
||||||
|
|
||||||
|
| Componente | Dati storici | Sessione in corso |
|
||||||
|
|---|---|---|
|
||||||
|
| Modello logistico | ogni puntata di ogni dossier | l'asta appena chiusa |
|
||||||
|
| Profilo per prodotto/ora | riepiloghi dei dossier | l'asta appena chiusa |
|
||||||
|
| Regime | — | risposte avversarie di quest'asta |
|
||||||
|
| Anticipo adattivo | margine salvato | ping e puntate di adesso |
|
||||||
|
| Duello | — | ritmo delle risposte di quest'asta |
|
||||||
|
|
||||||
|
## Dove si vede
|
||||||
|
|
||||||
|
Scheda **Apprendimento**: stato del modello, pesi, ultime decisioni, profilo per
|
||||||
|
prodotto, valutazione, e la sezione *Autonomia sul momento* con il modello di latenza e
|
||||||
|
il regime di ogni asta seguita.
|
||||||
|
|
||||||
|
Nel registro di ogni asta: `[REGIME] Calmo → Sfogo: …`, `[TIMING] … anticipo adattivo
|
||||||
|
portato a N ms`, `⛔ Strategia blocca: valore atteso negativo: P(senza risposta) … = −0,012 €`.
|
||||||
|
|
||||||
|
## Come si prova
|
||||||
|
|
||||||
|
- `Tests/AutonomyTests.cs` — regime (isteresi, raddoppio, tetto) e latenza (paletti,
|
||||||
|
asimmetria);
|
||||||
|
- `Tests/MlTests.cs`, `Tests/MlModelBacktest.cs` — modello e valutazione prequenziale;
|
||||||
|
- `Tests/AutoBidDuelTests.cs`, `Tests/BiddingHoursTests.cs` — duello e fascia oraria;
|
||||||
|
- `Tests/BacktestTests.cs`, `Tests/RealDossierBacktest.cs` — rigiocata sui dossier con
|
||||||
|
la stessa `BidStrategyService` del motore.
|
||||||
|
|
||||||
|
## Archiviazione
|
||||||
|
|
||||||
|
Ogni asta seguita ha **un solo** file: il dossier JSON Lines in `Registri/Aste/`
|
||||||
|
(intestazione, eventi coalescenti, riepilogo in coda). Le schede dettagliate si leggono
|
||||||
|
da lì (`AuctionDetailStore` legge testa e coda del file, con cache); l'archivio mensile
|
||||||
|
`aste-AAAA-MM.jsonl` che le duplicava non esiste più. Lo storico compatto delle aste
|
||||||
|
concluse resta in `Statistiche/completed-auctions.json`, con copia di sicurezza a ogni
|
||||||
|
pulizia.
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Ml
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Decide da solo l'anticipo con cui parte la puntata, a partire dalla rete di questa
|
||||||
|
/// sessione — entro i paletti dell'utente.
|
||||||
|
///
|
||||||
|
/// <para><b>Il problema.</b> L'anticipo giusto dipende da quanto ci mette la puntata ad
|
||||||
|
/// arrivare al server, e questo cambia da macchina a macchina, da rete a rete, da ora a
|
||||||
|
/// ora. Un numero fisso è sbagliato quasi sempre: troppo largo regala secondi agli
|
||||||
|
/// avversari, troppo stretto fa arrivare la puntata a giochi chiusi. Nei registri erano
|
||||||
|
/// entrambe le cose, in giorni diversi.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>La regola.</b> L'anticipo è un margine di sicurezza più la coda alta della
|
||||||
|
/// latenza misurata: <c>margine + p99(andata e ritorno)</c>. La coda, non la media —
|
||||||
|
/// una puntata su cento che arriva tardi costa un'asta intera, e la media non la vede.
|
||||||
|
/// Il margine copre l'elaborazione del server e l'arrotondamento al secondo con cui
|
||||||
|
/// Bidoo dichiara la scadenza.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Come impara.</b> Da tre cose che passano di qui comunque: il ping di ogni
|
||||||
|
/// interrogazione, l'andata e ritorno di ogni puntata, e l'esito «arrivata troppo
|
||||||
|
/// tardi». Una puntata tardiva alza il margine subito e di molto; una lunga serie di
|
||||||
|
/// puntate in tempo lo abbassa piano. Asimmetrico apposta: sbagliare per eccesso costa
|
||||||
|
/// qualche puntata in più, sbagliare per difetto costa l'asta.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>I paletti.</b> L'utente fissa minimo e massimo; il modello si muove solo lì
|
||||||
|
/// dentro. Un anticipo fissato a mano su una singola asta vince sempre.</para>
|
||||||
|
///
|
||||||
|
/// <para>Lo stato si salva fra una sessione e l'altra: la rete di casa è quasi sempre
|
||||||
|
/// la stessa, e ripartire da zero ogni volta significherebbe pagare la prima puntata
|
||||||
|
/// tardiva ogni giorno.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class LatencyModel
|
||||||
|
{
|
||||||
|
private const int Window = 300;
|
||||||
|
private static readonly object Sync = new();
|
||||||
|
|
||||||
|
private static readonly Queue<int> _pings = new();
|
||||||
|
private static readonly Queue<int> _roundTrips = new();
|
||||||
|
|
||||||
|
/// <summary>Margine oltre la coda della latenza. Parte da 300 ms: elaborazione del server più arrotondamento al secondo.</summary>
|
||||||
|
private static int _marginMs = 300;
|
||||||
|
|
||||||
|
private static int _bidsSinceLastLate;
|
||||||
|
private static int _lateBids;
|
||||||
|
private static int _bidsTotal;
|
||||||
|
private static bool _loaded;
|
||||||
|
|
||||||
|
private static string FilePath => Path.Combine(AppPaths.StatsFolder, "Apprendimento", "latenza.json");
|
||||||
|
|
||||||
|
// ── Alimentazione ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static void NotePing(int ms)
|
||||||
|
{
|
||||||
|
if (ms <= 0 || ms > 10000) return;
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
_pings.Enqueue(ms);
|
||||||
|
while (_pings.Count > Window) _pings.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Una puntata è stata inviata: quanto ci ha messo, e se è arrivata tardi.</summary>
|
||||||
|
public static void NoteBid(int roundTripMs, bool late)
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
if (roundTripMs > 0 && roundTripMs < 10000)
|
||||||
|
{
|
||||||
|
_roundTrips.Enqueue(roundTripMs);
|
||||||
|
while (_roundTrips.Count > Window) _roundTrips.Dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
_bidsTotal++;
|
||||||
|
|
||||||
|
if (late)
|
||||||
|
{
|
||||||
|
// Una sola puntata tardiva vale un'asta persa: si sale subito e di
|
||||||
|
// molto, poi si riscende piano se non ricapita.
|
||||||
|
_lateBids++;
|
||||||
|
_bidsSinceLastLate = 0;
|
||||||
|
_marginMs = Math.Min(2000, _marginMs + 150);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_bidsSinceLastLate++;
|
||||||
|
// Venti puntate in tempo di fila: si prova a stringere di un pelo.
|
||||||
|
if (_bidsSinceLastLate % 20 == 0) _marginMs = Math.Max(150, _marginMs - 25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Risposta ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>L'anticipo consigliato adesso, dentro i paletti delle impostazioni.</summary>
|
||||||
|
public static int RecommendedLeadMs(AppSettings settings)
|
||||||
|
{
|
||||||
|
Load();
|
||||||
|
|
||||||
|
var min = Math.Max(100, settings.LeadMinMs);
|
||||||
|
var max = Math.Max(min, settings.LeadMaxMs);
|
||||||
|
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
// La coda della latenza: l'andata e ritorno delle puntate se ne abbiamo
|
||||||
|
// abbastanza, altrimenti il ping delle interrogazioni, che è la stessa
|
||||||
|
// strada. Senza campioni si usa il predefinito dell'utente.
|
||||||
|
var coda = _roundTrips.Count >= 10 ? Percentile(_roundTrips, 0.99)
|
||||||
|
: _pings.Count >= 10 ? Percentile(_pings, 0.99)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
if (coda < 0) return Math.Clamp(settings.DefaultBidBeforeDeadlineMs, min, max);
|
||||||
|
|
||||||
|
return Math.Clamp(_marginMs + coda, min, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Numeri per la scheda e il registro.</summary>
|
||||||
|
public static (int Samples, int P50, int P99, int MarginMs, int LateBids, int Bids) Snapshot()
|
||||||
|
{
|
||||||
|
Load();
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
var src = _roundTrips.Count >= 10 ? _roundTrips : _pings;
|
||||||
|
return (src.Count,
|
||||||
|
src.Count > 0 ? Percentile(src, 0.50) : 0,
|
||||||
|
src.Count > 0 ? Percentile(src, 0.99) : 0,
|
||||||
|
_marginMs, _lateBids, _bidsTotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Percentile(Queue<int> q, double p)
|
||||||
|
{
|
||||||
|
var a = q.ToArray();
|
||||||
|
Array.Sort(a);
|
||||||
|
var i = (int)Math.Ceiling(p * a.Length) - 1;
|
||||||
|
return a[Math.Clamp(i, 0, a.Length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persistenza ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private sealed class Snap
|
||||||
|
{
|
||||||
|
public int MarginMs { get; set; }
|
||||||
|
public int LateBids { get; set; }
|
||||||
|
public int Bids { get; set; }
|
||||||
|
public int[] RoundTrips { get; set; } = Array.Empty<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Load()
|
||||||
|
{
|
||||||
|
if (_loaded) return;
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
if (_loaded) return;
|
||||||
|
_loaded = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(FilePath)) return;
|
||||||
|
var s = JsonSerializer.Deserialize<Snap>(File.ReadAllText(FilePath));
|
||||||
|
if (s == null) return;
|
||||||
|
_marginMs = Math.Clamp(s.MarginMs, 150, 2000);
|
||||||
|
_lateBids = s.LateBids;
|
||||||
|
_bidsTotal = s.Bids;
|
||||||
|
foreach (var r in s.RoundTrips) _roundTrips.Enqueue(r);
|
||||||
|
}
|
||||||
|
catch { /* si riparte dai predefiniti */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json;
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
json = JsonSerializer.Serialize(new Snap
|
||||||
|
{
|
||||||
|
MarginMs = _marginMs,
|
||||||
|
LateBids = _lateBids,
|
||||||
|
Bids = _bidsTotal,
|
||||||
|
RoundTrips = _roundTrips.ToArray()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(FilePath)!);
|
||||||
|
File.WriteAllText(FilePath, json);
|
||||||
|
}
|
||||||
|
catch { /* la prossima puntata riproverà */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Dimentica le misure, in memoria e su disco: si riparte dai predefiniti.</summary>
|
||||||
|
public static void Forget()
|
||||||
|
{
|
||||||
|
ResetForTests();
|
||||||
|
try { if (File.Exists(FilePath)) File.Delete(FilePath); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Solo per i test.</summary>
|
||||||
|
public static void ResetForTests(int marginMs = 300)
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
_pings.Clear();
|
||||||
|
_roundTrips.Clear();
|
||||||
|
_marginMs = marginMs;
|
||||||
|
_bidsSinceLastLate = 0;
|
||||||
|
_lateBids = 0;
|
||||||
|
_bidsTotal = 0;
|
||||||
|
_loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -175,6 +175,23 @@ namespace AutoBidder.Ml
|
|||||||
/// modello abbia imparato da dati sbagliati. Non tocca i dossier.
|
/// modello abbia imparato da dati sbagliati. Non tocca i dossier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Task RetrainFromScratchAsync(AppSettings settings)
|
public static Task RetrainFromScratchAsync(AppSettings settings)
|
||||||
|
{
|
||||||
|
Forget();
|
||||||
|
|
||||||
|
OnLog?.Invoke("[APPRENDIMENTO] Ricomincio da capo: profilo dallo storico, poi tutti i dossier.");
|
||||||
|
BootstrapProfileFromHistory();
|
||||||
|
|
||||||
|
// Un riaddestramento voluto ha diritto a tutto il tempo che serve.
|
||||||
|
var generose = new AppSettings { LearningBootstrapSecondsPerStart = Math.Max(600, settings.LearningBootstrapSecondsPerStart) };
|
||||||
|
return Task.Run(() => BootstrapAsync(generose));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dimentica tutto: modello, profilo, elenco dei dossier studiati, decisioni e
|
||||||
|
/// valutazione — in memoria e su disco. Senza, il modello ancora vivo riscriverebbe
|
||||||
|
/// il file al primo salvataggio. Al prossimo avvio si ristudiano i dossier da capo.
|
||||||
|
/// </summary>
|
||||||
|
public static void Forget()
|
||||||
{
|
{
|
||||||
lock (Sync)
|
lock (Sync)
|
||||||
{
|
{
|
||||||
@@ -190,7 +207,7 @@ namespace AutoBidder.Ml
|
|||||||
{
|
{
|
||||||
lock (SaveSync)
|
lock (SaveSync)
|
||||||
{
|
{
|
||||||
foreach (var f in new[] { ModelFile, ProfileFile, ManifestFile })
|
foreach (var f in new[] { ModelFile, ProfileFile, ManifestFile, EvaluationFile })
|
||||||
if (File.Exists(f)) File.Delete(f);
|
if (File.Exists(f)) File.Delete(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,13 +215,6 @@ namespace AutoBidder.Ml
|
|||||||
{
|
{
|
||||||
OnLog?.Invoke($"[APPRENDIMENTO] Archivio non cancellato del tutto: {ex.Message}");
|
OnLog?.Invoke($"[APPRENDIMENTO] Archivio non cancellato del tutto: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
OnLog?.Invoke("[APPRENDIMENTO] Ricomincio da capo: profilo dallo storico, poi tutti i dossier.");
|
|
||||||
BootstrapProfileFromHistory();
|
|
||||||
|
|
||||||
// Un riaddestramento voluto ha diritto a tutto il tempo che serve.
|
|
||||||
var generose = new AppSettings { LearningBootstrapSecondsPerStart = Math.Max(600, settings.LearningBootstrapSecondsPerStart) };
|
|
||||||
return Task.Run(() => BootstrapAsync(generose));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Avvio ────────────────────────────────────────────────────────
|
// ── Avvio ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ namespace AutoBidder.Models
|
|||||||
/// Es: 200ms = punta 200ms prima che il timer raggiunga 0.
|
/// Es: 200ms = punta 200ms prima che il timer raggiunga 0.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int BidBeforeDeadlineMs { get; set; } = 200;
|
public int BidBeforeDeadlineMs { get; set; } = 200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L'anticipo di quest'asta è stato scritto a mano dall'utente. Solo allora
|
||||||
|
/// <see cref="BidBeforeDeadlineMs"/> comanda: altrimenti il cecchino lo lascia
|
||||||
|
/// decidere al modello di latenza (vedi Ml/LatencyModel) e scrive qui il valore
|
||||||
|
/// scelto, così la griglia mostra l'anticipo davvero in uso.
|
||||||
|
/// </summary>
|
||||||
|
public bool BidLeadIsManual { get; set; }
|
||||||
|
|
||||||
public double MinPrice { get; set; } = 0;
|
public double MinPrice { get; set; } = 0;
|
||||||
public double MaxPrice { get; set; } = 0;
|
public double MaxPrice { get; set; } = 0;
|
||||||
@@ -528,43 +536,6 @@ namespace AutoBidder.Models
|
|||||||
public double AverageLatencyMs => LatencyHistory.Count > 0
|
public double AverageLatencyMs => LatencyHistory.Count > 0
|
||||||
? LatencyHistory.Average()
|
? LatencyHistory.Average()
|
||||||
: PollingLatencyMs > 0 ? PollingLatencyMs : 60;
|
: PollingLatencyMs > 0 ? PollingLatencyMs : 60;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Heat metric (0-100) che indica quanto � "calda" l'asta
|
|
||||||
/// Calcolato in base a: bidder attivi, frequenza puntate, collisioni
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public int HeatMetric { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Numero di bidder unici attivi negli ultimi N secondi
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public int ActiveBiddersCount { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Numero di collisioni rilevate (puntate nello stesso secondo)
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public int CollisionCount { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Collisioni consecutive senza puntata vincente
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public int ConsecutiveCollisions { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp dell'ultimo soft retreat
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public DateTime? LastSoftRetreatAt { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Se true, l'asta � in soft retreat temporaneo
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public bool IsInSoftRetreat { get; set; } = false;
|
|
||||||
|
|
||||||
// ── Duello con l'autopuntata avversaria ──────────────────────────
|
// ── Duello con l'autopuntata avversaria ──────────────────────────
|
||||||
// Vedi Utilities/AutoBidDuel per il perche' e per i numeri misurati.
|
// Vedi Utilities/AutoBidDuel per il perche' e per i numeri misurati.
|
||||||
@@ -584,6 +555,14 @@ namespace AutoBidder.Models
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool AutoBidDuelDetected { get; set; }
|
public bool AutoBidDuelDetected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Il regime di concorrenza di quest'asta: quando lasciar sfogare gli altri e
|
||||||
|
/// quando tornare a puntare. Vedi <see cref="Ml.CompetitionRegime"/>.
|
||||||
|
/// Stato di sessione: al riavvio riparte Calmo, e va bene così.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public Ml.CompetitionRegime Regime { get; } = new();
|
||||||
|
|
||||||
// ── Apprendimento: l'ultima risposta del modello per quest'asta ──
|
// ── Apprendimento: l'ultima risposta del modello per quest'asta ──
|
||||||
// Vedi Ml/LearningService. Non si salvano: valgono per l'istante in cui sono state
|
// Vedi Ml/LearningService. Non si salvano: valgono per l'istante in cui sono state
|
||||||
// calcolate, e al riavvio non ci sarebbe piu' lo stato che le ha prodotte.
|
// calcolate, e al riavvio non ci sarebbe piu' lo stato che le ha prodotte.
|
||||||
@@ -612,6 +591,9 @@ namespace AutoBidder.Models
|
|||||||
|
|
||||||
AutoResponsesInARow = Utilities.AutoBidDuel.Aggiorna(AutoResponsesInARow, ritardo);
|
AutoResponsesInARow = Utilities.AutoBidDuel.Aggiorna(AutoResponsesInARow, ritardo);
|
||||||
|
|
||||||
|
// Il regime vuole sapere se una puntata di prova è stata coperta subito.
|
||||||
|
Regime.RispostaAvversaria(ritardo);
|
||||||
|
|
||||||
if (AutoBidDuelDetected) return false;
|
if (AutoBidDuelDetected) return false;
|
||||||
if (!Utilities.AutoBidDuel.DuelloRiconosciuto(AutoResponsesInARow, soglia)) return false;
|
if (!Utilities.AutoBidDuel.DuelloRiconosciuto(AutoResponsesInARow, soglia)) return false;
|
||||||
|
|
||||||
@@ -643,12 +625,6 @@ namespace AutoBidder.Models
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public int FailedBidCount { get; set; } = 0;
|
public int FailedBidCount { get; set; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lista utenti identificati come aggressivi in questa asta
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public HashSet<string> AggressiveBidders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Offset dinamico calcolato per questa asta (ms)
|
/// Offset dinamico calcolato per questa asta (ms)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -700,21 +676,10 @@ namespace AutoBidder.Models
|
|||||||
// IMPOSTAZIONI PER-ASTA (override globali)
|
// IMPOSTAZIONI PER-ASTA (override globali)
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Override: abilita/disabilita strategie avanzate per questa asta
|
|
||||||
/// null = usa impostazione globale
|
|
||||||
/// </summary>
|
|
||||||
public bool? AdvancedStrategiesEnabled { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Override: abilita/disabilita jitter per questa asta
|
/// Override: abilita/disabilita jitter per questa asta
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? JitterEnabledOverride { get; set; }
|
public bool? JitterEnabledOverride { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Override: abilita/disabilita soft retreat per questa asta
|
|
||||||
/// </summary>
|
|
||||||
public bool? SoftRetreatEnabledOverride { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Override per questa asta della sospensione a fascia oraria.
|
/// Override per questa asta della sospensione a fascia oraria.
|
||||||
@@ -730,25 +695,6 @@ namespace AutoBidder.Models
|
|||||||
|
|
||||||
// ?? NUOVO: Rilevamento situazione di duello
|
// ?? NUOVO: Rilevamento situazione di duello
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True se rilevata situazione di duello (solo 2 bidder dominanti)
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public bool IsDuelSituation { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Username dell'avversario in caso di duello
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public string? DuelOpponent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vantaggio/svantaggio nel duello (% puntate mie - % puntate avversario)
|
|
||||||
/// Positivo = sto dominando, Negativo = sto perdendo
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public double DuelAdvantage { get; set; } = 0;
|
|
||||||
|
|
||||||
// ???????????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????????
|
||||||
// GESTIONE MEMORIA
|
// GESTIONE MEMORIA
|
||||||
// ???????????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????????
|
||||||
@@ -775,13 +721,10 @@ namespace AutoBidder.Models
|
|||||||
LatencyHistory?.Clear();
|
LatencyHistory?.Clear();
|
||||||
LatencyHistory = null!;
|
LatencyHistory = null!;
|
||||||
|
|
||||||
AggressiveBidders?.Clear();
|
|
||||||
AggressiveBidders = null!;
|
|
||||||
|
|
||||||
// Pulisci oggetti complessi
|
// Pulisci oggetti complessi
|
||||||
LastState = null;
|
LastState = null;
|
||||||
CalculatedValue = null;
|
CalculatedValue = null;
|
||||||
DuelOpponent = null;
|
|
||||||
WinLimitDescription = null;
|
WinLimitDescription = null;
|
||||||
|
|
||||||
// Reset flag
|
// Reset flag
|
||||||
|
|||||||
@@ -282,7 +282,9 @@ namespace AutoBidder.Services
|
|||||||
_monitoringCts = new CancellationTokenSource();
|
_monitoringCts = new CancellationTokenSource();
|
||||||
_supervisorTask = Task.Run(() => SupervisorLoop(_monitoringCts.Token));
|
_supervisorTask = Task.Run(() => SupervisorLoop(_monitoringCts.Token));
|
||||||
|
|
||||||
OnLog?.Invoke($"[START] Monitoraggio avviato (poll {settings.PollIntervalCriticalMs}-{settings.PollIntervalFarMs}ms, max {settings.MaxRequestsPerSecond:F0} req/s)");
|
OnLog?.Invoke(settings.MaxRequestsPerSecond > 0
|
||||||
|
? $"[START] Monitoraggio avviato (max {settings.MaxRequestsPerSecond:F0} richieste/s)"
|
||||||
|
: "[START] Monitoraggio avviato (nessun tetto alle richieste: frena solo se il server lo chiede)");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
@@ -430,6 +432,7 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
||||||
|
Ml.LatencyModel.NotePing(state.PollingLatencyMs);
|
||||||
|
|
||||||
// Serie prezzi: va raccolta ora, a posteriori Bidoo non la espone.
|
// Serie prezzi: va raccolta ora, a posteriori Bidoo non la espone.
|
||||||
auction.TrackPrice(state.Price);
|
auction.TrackPrice(state.Price);
|
||||||
@@ -641,30 +644,7 @@ namespace AutoBidder.Services
|
|||||||
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
||||||
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg}");
|
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg}");
|
||||||
|
|
||||||
// Feedback per aste perse
|
|
||||||
var settings = SettingsManager.Load();
|
var settings = SettingsManager.Load();
|
||||||
if (!won && settings.ShowLateBidWarning)
|
|
||||||
{
|
|
||||||
// Se abbiamo provato a puntare ma fallito con errore timer
|
|
||||||
var lastBidAttempt = auction.BidHistory
|
|
||||||
.Where(b => b.EventType == BidEventType.MyBid && !b.Success)
|
|
||||||
.OrderByDescending(b => b.Timestamp)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (lastBidAttempt != null &&
|
|
||||||
(lastBidAttempt.Notes?.Contains("timer") == true ||
|
|
||||||
lastBidAttempt.Notes?.Contains("scaduto") == true))
|
|
||||||
{
|
|
||||||
int currentOffset = auction.BidBeforeDeadlineMs > 0
|
|
||||||
? auction.BidBeforeDeadlineMs
|
|
||||||
: settings.DefaultBidBeforeDeadlineMs;
|
|
||||||
|
|
||||||
auction.AddLog($"[⚠️ SUGGERIMENTO] Puntata arrivata troppo tardi! " +
|
|
||||||
$"Tempo attuale: {currentOffset}ms. " +
|
|
||||||
$"Prova ad aumentarlo a {currentOffset + 500}ms o più.");
|
|
||||||
OnLog?.Invoke($"[LATE] {auction.Name}: aumenta il tempo di puntata (attuale: {currentOffset}ms)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auction.BidHistory.Add(new BidHistory
|
auction.BidHistory.Add(new BidHistory
|
||||||
{
|
{
|
||||||
@@ -759,10 +739,15 @@ namespace AutoBidder.Services
|
|||||||
auction.LastClickAt = DateTime.UtcNow;
|
auction.LastClickAt = DateTime.UtcNow;
|
||||||
|
|
||||||
// Registra metriche
|
// Registra metriche
|
||||||
bool isCollision = result.Error?.Contains("timer") == true || result.Error?.Contains("scaduto") == true;
|
bool isCollision = !result.Success && IsLateBidError(result.Error);
|
||||||
_bidStrategy.RecordBidAttempt(auction, result.Success, collision: isCollision);
|
_bidStrategy.RecordBidAttempt(auction, result.Success);
|
||||||
|
|
||||||
if (!result.Success && isCollision)
|
// Il modello di latenza impara da ogni puntata: quanto ci ha messo, e se è
|
||||||
|
// arrivata tardi. Una tardiva alza l'anticipo subito; molte in tempo lo
|
||||||
|
// abbassano piano.
|
||||||
|
Ml.LatencyModel.NoteBid(result.LatencyMs, late: isCollision);
|
||||||
|
|
||||||
|
if (isCollision)
|
||||||
{
|
{
|
||||||
_bidStrategy.RecordTimerExpired(auction);
|
_bidStrategy.RecordTimerExpired(auction);
|
||||||
}
|
}
|
||||||
@@ -809,19 +794,18 @@ namespace AutoBidder.Services
|
|||||||
auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
|
auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
|
||||||
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
|
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
|
||||||
|
|
||||||
// Feedback per puntata tardiva
|
// Puntata tardiva: l'anticipo lo corregge da solo il modello di
|
||||||
|
// latenza (è già stato avvisato sopra). Qui si dice solo cosa ha deciso.
|
||||||
if (isLateBid && settings.ShowLateBidWarning)
|
if (isLateBid && settings.ShowLateBidWarning)
|
||||||
{
|
{
|
||||||
int currentOffset = auction.BidBeforeDeadlineMs > 0
|
var adattivo = Ml.LatencyModel.RecommendedLeadMs(settings);
|
||||||
? auction.BidBeforeDeadlineMs
|
var manuale = auction.BidLeadIsManual && auction.BidBeforeDeadlineMs > 0;
|
||||||
: settings.DefaultBidBeforeDeadlineMs;
|
|
||||||
|
auction.AddLog($"[TIMING] Puntata arrivata troppo tardi (latenza ~{pollingPing + result.LatencyMs}ms). " +
|
||||||
int suggestedOffset = currentOffset + 300 + pollingPing;
|
(manuale
|
||||||
|
? $"L'anticipo di quest'asta è fisso a {auction.BidBeforeDeadlineMs}ms: il modello consiglierebbe {adattivo}ms."
|
||||||
auction.AddLog($"[⚠️ TIMING] Puntata arrivata troppo tardi! " +
|
: $"Anticipo adattivo portato a {adattivo}ms."));
|
||||||
$"Offset attuale: {currentOffset}ms. Latenza totale: ~{pollingPing + result.LatencyMs}ms. " +
|
OnLog?.Invoke($"[LATE] {auction.Name}: puntata tardiva, anticipo adattivo ora {adattivo}ms");
|
||||||
$"Suggerimento: aumenta a {suggestedOffset}ms");
|
|
||||||
OnLog?.Invoke($"[LATE] {auction.Name}: puntata tardiva, aumenta offset a {suggestedOffset}ms");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,44 +858,6 @@ namespace AutoBidder.Services
|
|||||||
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Value);
|
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONTROLLO ANTI-COLLISIONE (OPZIONALE)
|
|
||||||
if (settings.HardcodedAntiCollisionEnabled)
|
|
||||||
{
|
|
||||||
var recentBidsThreshold = 10;
|
|
||||||
var maxActiveBidders = 3;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
||||||
var recentBids = auction.RecentBids
|
|
||||||
.Where(b => now - b.Timestamp <= recentBidsThreshold)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var activeBidders = recentBids
|
|
||||||
.Select(b => b.Username)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Count();
|
|
||||||
|
|
||||||
auction.AddLog($"Competizione: {activeBidders} bidder attivi (soglia={maxActiveBidders})",
|
|
||||||
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Competition);
|
|
||||||
|
|
||||||
if (activeBidders >= maxActiveBidders)
|
|
||||||
{
|
|
||||||
var session = _apiClient.GetSession();
|
|
||||||
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
|
||||||
|
|
||||||
if (lastBid != null &&
|
|
||||||
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
auction.AddLog($"⛔ Asta affollata: {activeBidders} bidder attivi",
|
|
||||||
Models.AuctionLogLevel.Strategy, Models.AuctionLogCategory.Competition);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
// CONTROLLO 1: Limite minimo puntate residue
|
// CONTROLLO 1: Limite minimo puntate residue
|
||||||
if (settings.MinimumRemainingBids > 0)
|
if (settings.MinimumRemainingBids > 0)
|
||||||
{
|
{
|
||||||
@@ -1209,11 +1155,12 @@ namespace AutoBidder.Services
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Una puntata avversaria: se arriva col ritardo tipico dell'autopuntata
|
// Una puntata avversaria: quanto ha tardato rispetto alla nostra alimenta
|
||||||
// (~6 s dalla nostra) alimenta il conteggio del duello.
|
// il riconoscimento del duello (~6 s = autopuntata) e il regime di
|
||||||
|
// concorrenza (una puntata di prova coperta subito). Si misura sempre;
|
||||||
|
// l'impostazione decide solo se il duello riconosciuto ferma le puntate.
|
||||||
var impostazioni = SettingsManager.Load();
|
var impostazioni = SettingsManager.Load();
|
||||||
if (impostazioni.AutoBidDuelWithdrawEnabled &&
|
if (auction.NoteOpponentResponse(DateTime.UtcNow, impostazioni.AutoBidDuelResponses))
|
||||||
auction.NoteOpponentResponse(DateTime.UtcNow, impostazioni.AutoBidDuelResponses))
|
|
||||||
{
|
{
|
||||||
auction.AddLog(AutoBidDuel.Spiegazione(auction.AutoResponsesInARow),
|
auction.AddLog(AutoBidDuel.Spiegazione(auction.AutoResponsesInARow),
|
||||||
AuctionLogLevel.Warning, AuctionLogCategory.Strategy);
|
AuctionLogLevel.Warning, AuctionLogCategory.Strategy);
|
||||||
|
|||||||
@@ -1,359 +1,62 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
using AutoBidder.Utilities;
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
namespace AutoBidder.Services
|
namespace AutoBidder.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Servizio per strategie avanzate di puntata.
|
/// Decide se puntare. Quattro passi, sempre nello stesso ordine — vedi
|
||||||
/// Implementa: adaptive latency, jitter, dynamic offset, heat metric,
|
/// <c>Ml/LEGGIMI.md</c> per il disegno completo e i numeri che lo giustificano.
|
||||||
/// competition detection, soft retreat, probabilistic bidding, opponent profiling.
|
///
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item><b>Paletti</b> — regole dell'utente che non si discutono: tetti di
|
||||||
|
/// puntate e di spesa, fascia oraria sospesa. Non imparano niente: sono il
|
||||||
|
/// perimetro dentro cui tutto il resto è libero di muoversi.</item>
|
||||||
|
/// <item><b>Duello</b> — riconoscimento dell'autopuntata avversaria dal ritmo
|
||||||
|
/// delle risposte. È il segnale più forte che esista sul momento, e informa il
|
||||||
|
/// regime prima ancora del modello.</item>
|
||||||
|
/// <item><b>Valore atteso</b> — il modello appreso dice con che probabilità una
|
||||||
|
/// puntata fatta adesso resterebbe senza risposta; moltiplicata per il margine e
|
||||||
|
/// al netto del costo dà quanto vale, in euro, puntare in questo istante.</item>
|
||||||
|
/// <item><b>Regime</b> — una macchina a stati per asta che trasforma la sequenza
|
||||||
|
/// dei valori attesi in una condotta: quando lasciar sfogare gli altri, quando
|
||||||
|
/// sondare, quando tornare a giocare. Con isteresi, perché ogni rientro
|
||||||
|
/// sbagliato costa una puntata.</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// <para>Le vecchie euristiche a soglia fissa — calore, ritiro morbido, avversari
|
||||||
|
/// aggressivi, anti-bot, puntata probabilistica, velocità del prezzo — non ci sono
|
||||||
|
/// più: rigiocate sui dossier non hanno mai bloccato una puntata sbagliata senza
|
||||||
|
/// bloccarne anche di giuste, e i loro numeri magici andavano tarati a mano. Il
|
||||||
|
/// modello e il regime imparano dai dati storici e dalla sessione in corso.</para>
|
||||||
|
///
|
||||||
|
/// <para><i>Quando</i> puntare — l'anticipo — non si decide qui ma nel cecchino,
|
||||||
|
/// con <see cref="Ml.LatencyModel"/>: anche quello si adatta alla rete della
|
||||||
|
/// sessione, dentro i paletti dell'utente.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BidStrategyService
|
public class BidStrategyService
|
||||||
{
|
{
|
||||||
private readonly Random _random = new();
|
|
||||||
private int _sessionTotalBids = 0;
|
private int _sessionTotalBids = 0;
|
||||||
private DateTime _sessionStartedAt = DateTime.UtcNow;
|
private DateTime _sessionStartedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Aggiorna heat metric per un'asta
|
|
||||||
/// </summary>
|
|
||||||
public void UpdateHeatMetric(AuctionInfo auction, AppSettings settings, string currentUsername = "")
|
|
||||||
{
|
|
||||||
if (!settings.CompetitionDetectionEnabled) return;
|
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
||||||
var windowStart = now - settings.CompetitionWindowSeconds;
|
|
||||||
|
|
||||||
// Conta bidder unici nella finestra temporale (escludo me stesso)
|
|
||||||
var recentBids = auction.RecentBids
|
|
||||||
.Where(b => b.Timestamp >= windowStart)
|
|
||||||
.Where(b => !b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
auction.ActiveBiddersCount = recentBids
|
|
||||||
.Select(b => b.Username)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Count();
|
|
||||||
|
|
||||||
// Conta collisioni (puntate nello stesso secondo)
|
|
||||||
var bidsBySecond = recentBids
|
|
||||||
.GroupBy(b => b.Timestamp)
|
|
||||||
.Where(g => g.Count() > 1)
|
|
||||||
.Count();
|
|
||||||
|
|
||||||
auction.CollisionCount = bidsBySecond;
|
|
||||||
|
|
||||||
// Calcola heat metric (0-100)
|
|
||||||
// Fattori: bidder attivi (40%), frequenza puntate (30%), collisioni (30%)
|
|
||||||
|
|
||||||
int bidderScore = Math.Min(auction.ActiveBiddersCount * 15, 40); // Max 40 punti
|
|
||||||
int frequencyScore = Math.Min(recentBids.Count * 3, 30); // Max 30 punti
|
|
||||||
int collisionScore = Math.Min(auction.CollisionCount * 10, 30); // Max 30 punti
|
|
||||||
|
|
||||||
auction.HeatMetric = bidderScore + frequencyScore + collisionScore;
|
|
||||||
|
|
||||||
// Identifica bidder aggressivi e situazioni di duello
|
|
||||||
if (settings.OpponentProfilingEnabled)
|
|
||||||
{
|
|
||||||
UpdateAggressiveBidders(auction, settings, currentUsername);
|
|
||||||
DetectDuelSituation(auction, settings, currentUsername);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Identifica e tracca bidder aggressivi (basato su ultime N puntate, esclude utente corrente)
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateAggressiveBidders(AuctionInfo auction, AppSettings settings, string currentUsername)
|
|
||||||
{
|
|
||||||
// ?? FIX: Usa finestra scorrevole di ultime N puntate
|
|
||||||
var windowSize = settings.AggressiveBidderWindowSize > 0 ? settings.AggressiveBidderWindowSize : 30;
|
|
||||||
var recentWindow = auction.RecentBids
|
|
||||||
.Take(windowSize)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var bidCounts = recentWindow
|
|
||||||
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(g => new { Username = g.Key, Count = g.Count(), Percentage = (double)g.Count() / recentWindow.Count * 100 })
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
auction.AggressiveBidders.Clear();
|
|
||||||
|
|
||||||
foreach (var bidder in bidCounts)
|
|
||||||
{
|
|
||||||
// ?? FIX: NON aggiungere l'utente corrente come aggressivo!
|
|
||||||
if (bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// ?? FIX: Soglia pi� permissiva - usa percentuale invece di conteggio assoluto
|
|
||||||
// Un bidder � "aggressivo" se ha pi� del 40% delle puntate nella finestra (configurabile)
|
|
||||||
var percentageThreshold = settings.AggressiveBidderPercentageThreshold > 0 ? settings.AggressiveBidderPercentageThreshold : 40.0;
|
|
||||||
|
|
||||||
if (bidder.Percentage >= percentageThreshold || bidder.Count >= settings.AggressiveBidderThreshold)
|
|
||||||
{
|
|
||||||
auction.AggressiveBidders.Add(bidder.Username);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rileva situazione di "duello" (solo 2 bidder attivi che si contendono l'asta)
|
|
||||||
/// In questa situazione bisogna essere pronti perch� se uno si ritira l'altro vince
|
|
||||||
/// </summary>
|
|
||||||
private void DetectDuelSituation(AuctionInfo auction, AppSettings settings, string currentUsername)
|
|
||||||
{
|
|
||||||
var windowSize = settings.DuelDetectionWindowSize > 0 ? settings.DuelDetectionWindowSize : 20;
|
|
||||||
var recentWindow = auction.RecentBids.Take(windowSize).ToList();
|
|
||||||
|
|
||||||
if (recentWindow.Count < 6) // Serve un minimo di puntate per rilevare un pattern
|
|
||||||
{
|
|
||||||
auction.IsDuelSituation = false;
|
|
||||||
auction.DuelOpponent = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bidders = recentWindow
|
|
||||||
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(g => new { Username = g.Key, Count = g.Count(), Percentage = (double)g.Count() / recentWindow.Count * 100 })
|
|
||||||
.OrderByDescending(b => b.Count)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// Duello: esattamente 2 bidder dominanti che coprono almeno l'80% delle puntate
|
|
||||||
if (bidders.Count >= 2)
|
|
||||||
{
|
|
||||||
var top2Percentage = bidders.Take(2).Sum(b => b.Percentage);
|
|
||||||
|
|
||||||
if (top2Percentage >= 80 && bidders.Count <= 3)
|
|
||||||
{
|
|
||||||
auction.IsDuelSituation = true;
|
|
||||||
|
|
||||||
// Trova l'avversario (chi NON sono io)
|
|
||||||
var opponent = bidders.FirstOrDefault(b =>
|
|
||||||
!b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
auction.DuelOpponent = opponent?.Username;
|
|
||||||
|
|
||||||
// Calcola chi sta dominando
|
|
||||||
var myStats = bidders.FirstOrDefault(b =>
|
|
||||||
b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
auction.DuelAdvantage = myStats != null && opponent != null
|
|
||||||
? myStats.Percentage - opponent.Percentage
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auction.IsDuelSituation = false;
|
|
||||||
auction.DuelOpponent = null;
|
|
||||||
auction.DuelAdvantage = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auction.IsDuelSituation = false;
|
|
||||||
auction.DuelOpponent = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifica se � il caso di puntare considerando tutte le strategie
|
|
||||||
/// </summary>
|
|
||||||
public BidDecision ShouldPlaceBid(AuctionInfo auction, AuctionState state, AppSettings settings, string currentUsername)
|
public BidDecision ShouldPlaceBid(AuctionInfo auction, AuctionState state, AppSettings settings, string currentUsername)
|
||||||
{
|
{
|
||||||
var decision = new BidDecision { ShouldBid = true };
|
var decision = new BidDecision { ShouldBid = true };
|
||||||
|
|
||||||
// Se le strategie avanzate sono disabilitate per questa asta, salta tutto
|
|
||||||
if (auction.AdvancedStrategiesEnabled == false)
|
|
||||||
{
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0. DUELLO CON L'AUTOPUNTATA — il primo controllo, perché è quello che
|
// ── 1. Paletti ──────────────────────────────────────────────────
|
||||||
// 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.
|
// Fascia oraria sospesa. L'asta NON viene fermata: resta Attiva, continua a
|
||||||
//
|
// essere seguita e riprende a puntare da sola alla fine della fascia. Su 6060
|
||||||
// L'asta NON viene fermata: resta Attiva, continua a essere seguita e
|
// aste concluse la stessa asta costa quasi il doppio alle 0 e alle 9 rispetto
|
||||||
// riprende a puntare da sola alla fine della fascia. È voluto — le aste
|
// alle 10-13: vedi BiddingHours.
|
||||||
// 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) &&
|
if (QuietHoursApply(auction, settings) &&
|
||||||
Utilities.BiddingHours.IsQuiet(DateTime.Now, settings.QuietHoursStart, settings.QuietHoursEnd))
|
BiddingHours.IsQuiet(DateTime.Now, settings.QuietHoursStart, settings.QuietHoursEnd))
|
||||||
{
|
{
|
||||||
decision.ShouldBid = false;
|
decision.ShouldBid = false;
|
||||||
decision.Reason = Utilities.BiddingHours.Spiegazione(
|
decision.Reason = BiddingHours.Spiegazione(settings.QuietHoursStart, settings.QuietHoursEnd);
|
||||||
settings.QuietHoursStart, settings.QuietHoursEnd);
|
|
||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0-ter. VALORE ATTESO APPRESO.
|
|
||||||
//
|
|
||||||
// Il modello dice con che probabilità una puntata fatta adesso resterebbe
|
|
||||||
// senza risposta; il valore atteso è quella probabilità per il margine che
|
|
||||||
// resta (valore meno prezzo meno il centesimo), meno il costo della puntata.
|
|
||||||
// Sotto zero, in media, puntare perde soldi.
|
|
||||||
//
|
|
||||||
// Il modello parla sempre, ma decide solo quando ha appreso abbastanza aste:
|
|
||||||
// un modello appena nato direbbe cose a caso, e a caso fermerebbe le puntate.
|
|
||||||
// La probabilità e il valore atteso finiscono comunque sull'asta, per il
|
|
||||||
// registro e per l'interfaccia.
|
|
||||||
if (settings.LearningGateEnabled && auction.BuyNowPrice is > 0)
|
|
||||||
{
|
|
||||||
var prob = Ml.LearningService.PredictUnanswered(auction, state, DateTime.Now, currentUsername);
|
|
||||||
if (prob is { } p)
|
|
||||||
{
|
|
||||||
var costo = settings.AverageBidCostEuro * Math.Max(0.1, settings.LearningEvMultiplier);
|
|
||||||
var margine = auction.BuyNowPrice.Value - state.Price - 0.01;
|
|
||||||
var ev = p * margine - costo;
|
|
||||||
|
|
||||||
auction.LearnedUnansweredProbability = p;
|
|
||||||
auction.LearnedExpectedValue = ev;
|
|
||||||
|
|
||||||
var pronto = Ml.LearningService.IsReady(settings);
|
|
||||||
Ml.LearningService.RecordDecision(auction.Name, state.Price, p, ev, blocked: pronto && ev < 0, ready: pronto);
|
|
||||||
|
|
||||||
if (pronto && ev < 0)
|
|
||||||
{
|
|
||||||
decision.ShouldBid = false;
|
|
||||||
decision.Reason =
|
|
||||||
$"valore atteso negativo: P(senza risposta) {p:P2} × margine {margine:F2} € " +
|
|
||||||
$"− puntata {costo:F2} € = {ev:+0.000;-0.000} €";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ? RIMOSSO: Entry Point - Era sbagliato!
|
|
||||||
// I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
|
||||||
// Se l'utente imposta MaxPrice=2�, vuole puntare FINO A 2�, non fino al 70%!
|
|
||||||
// I controlli MinPrice/MaxPrice sono gi� gestiti in AuctionMonitor.ShouldBid()
|
|
||||||
// L'Entry Point pu� essere usato SOLO per calcolare limiti CONSIGLIATI, non per bloccare.
|
|
||||||
|
|
||||||
// 1. ANTI-BOT — riconoscimento del puntatore a cadenza fissa.
|
|
||||||
//
|
|
||||||
// Spento di proposito (vedi AppSettings.AntiBotDetectionEnabled): rigiocando i
|
|
||||||
// dossier raccolti la regola rifiutava fra il 4% e il 9% delle puntate, a seconda
|
|
||||||
// dell'anticipo. Chi lo accende lo fa sapendo che e' una scelta di
|
|
||||||
// prudenza, non una difesa: un avversario a cadenza fissa è il più facile da
|
|
||||||
// battere, perché punta sempre con secondi di anticipo.
|
|
||||||
if (settings.AntiBotDetectionEnabled && !string.IsNullOrEmpty(state.LastBidder))
|
|
||||||
{
|
|
||||||
var botCheck = DetectBotPattern(auction, state.LastBidder, currentUsername);
|
|
||||||
if (botCheck.IsBot)
|
|
||||||
{
|
|
||||||
decision.ShouldBid = false;
|
|
||||||
decision.Reason = $"Anti-bot: {state.LastBidder} punta a cadenza fissa ({botCheck.GapSeconds:F0}s)";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ?? 2. USER EXHAUSTION - Sfrutta utenti stanchi (info solo, non blocca)
|
|
||||||
if (settings.UserExhaustionEnabled && !string.IsNullOrEmpty(state.LastBidder))
|
|
||||||
{
|
|
||||||
var exhaustionCheck = CheckUserExhaustion(auction, state.LastBidder, currentUsername);
|
|
||||||
// Non blocchiamo, ma potremmo loggare per info
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Verifica soft retreat
|
|
||||||
if (settings.SoftRetreatEnabled || (auction.SoftRetreatEnabledOverride ?? settings.SoftRetreatEnabled))
|
|
||||||
{
|
|
||||||
if (auction.IsInSoftRetreat)
|
|
||||||
{
|
|
||||||
var retreatEnd = auction.LastSoftRetreatAt?.AddSeconds(settings.SoftRetreatDurationSeconds);
|
|
||||||
if (retreatEnd > DateTime.UtcNow)
|
|
||||||
{
|
|
||||||
decision.ShouldBid = false;
|
|
||||||
decision.Reason = $"Soft retreat attivo (termina tra {(retreatEnd.Value - DateTime.UtcNow).TotalSeconds:F0}s)";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Fine soft retreat
|
|
||||||
auction.IsInSoftRetreat = false;
|
|
||||||
auction.ConsecutiveCollisions = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifica se attivare soft retreat
|
|
||||||
if (auction.ConsecutiveCollisions >= settings.SoftRetreatAfterCollisions)
|
|
||||||
{
|
|
||||||
auction.IsInSoftRetreat = true;
|
|
||||||
auction.LastSoftRetreatAt = DateTime.UtcNow;
|
|
||||||
decision.ShouldBid = false;
|
|
||||||
decision.Reason = $"Soft retreat attivato dopo {auction.ConsecutiveCollisions} collisioni";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Verifica competition threshold
|
|
||||||
if (settings.CompetitionDetectionEnabled)
|
|
||||||
{
|
|
||||||
if (auction.ActiveBiddersCount >= settings.CompetitionThreshold)
|
|
||||||
{
|
|
||||||
// Controlla se l'ultimo bidder sono io - se s�, posso continuare
|
|
||||||
var lastBid = auction.RecentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
|
||||||
if (lastBid != null && !lastBid.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (settings.AutoPauseHotAuctions && auction.HeatMetric >= settings.HeatThresholdForPause)
|
|
||||||
{
|
|
||||||
decision.ShouldBid = false;
|
|
||||||
decision.Reason = $"Asta troppo calda (heat={auction.HeatMetric}%, bidder={auction.ActiveBiddersCount})";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Verifica opponent profiling
|
|
||||||
if (settings.OpponentProfilingEnabled && auction.AggressiveBidders.Count > 0)
|
|
||||||
{
|
|
||||||
if (settings.AggressiveBidderAction == "Avoid")
|
|
||||||
{
|
|
||||||
decision.ShouldBid = false;
|
|
||||||
decision.Reason = $"Bidder aggressivi rilevati: {string.Join(", ", auction.AggressiveBidders.Take(3))}";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Probabilistic bidding
|
|
||||||
if (settings.ProbabilisticBiddingEnabled)
|
|
||||||
{
|
|
||||||
var probability = CalculateBidProbability(auction, settings);
|
|
||||||
var roll = _random.NextDouble();
|
|
||||||
|
|
||||||
if (roll > probability)
|
|
||||||
{
|
|
||||||
decision.ShouldBid = false;
|
|
||||||
decision.Reason = $"Skip probabilistico (p={probability:P0}, roll={roll:P0})";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Bankroll manager
|
|
||||||
if (settings.BankrollManagerEnabled)
|
if (settings.BankrollManagerEnabled)
|
||||||
{
|
{
|
||||||
var bankrollCheck = CheckBankrollLimits(auction, settings);
|
var bankrollCheck = CheckBankrollLimits(auction, settings);
|
||||||
@@ -364,150 +67,90 @@ namespace AutoBidder.Services
|
|||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ? RIMOSSO: DetectLastSecondSniper - causava falsi positivi
|
// ── 2. Duello con l'autopuntata ─────────────────────────────────
|
||||||
// In un duello, TUTTI i bidder hanno pattern regolari (ogni reset del timer)
|
|
||||||
// Questa strategia bloccava puntate legittime e faceva perdere aste
|
|
||||||
|
|
||||||
// 7. VELOCITA' DEL PREZZO - l'asta sta salendo troppo in fretta.
|
|
||||||
//
|
//
|
||||||
// La soglia era fissa a 0,10 EUR/s, cioe' dieci puntate al secondo: su 40.000
|
// L'autopuntata del sito scatta a 2 secondi e si riarma ogni volta che il suo
|
||||||
// valutazioni riprese dai dossier non e' scattata mai una volta, e il massimo
|
// padrone perde la testa: risponde sempre, in ~6 secondi, e non si stanca.
|
||||||
// mai osservato e' 0,016 EUR/s. Ora e' un'impostazione, spenta di predefinito:
|
// Nelle prove reali il motore ci ha lasciato 191 puntate su tre aste per zero
|
||||||
// un controllo che non puo' scattare da' una falsa sensazione di protezione.
|
// vittorie. Il riconoscimento vive in AutoBidDuel; qui si applica e si passa
|
||||||
if (settings.PriceVelocityBlockPerSecond > 0)
|
// l'informazione al regime, che da quel momento pretende più pazienza.
|
||||||
|
if (auction.AutoBidDuelDetected)
|
||||||
{
|
{
|
||||||
var priceVelocity = CalculatePriceVelocity(auction);
|
auction.Regime.AutopuntataRiconosciuta();
|
||||||
if (priceVelocity > settings.PriceVelocityBlockPerSecond)
|
|
||||||
|
if (settings.AutoBidDuelWithdrawEnabled)
|
||||||
{
|
{
|
||||||
decision.ShouldBid = false;
|
decision.ShouldBid = false;
|
||||||
decision.Reason = $"Prezzo sale troppo in fretta ({priceVelocity:F3} EUR/s, soglia {settings.PriceVelocityBlockPerSecond:F3})";
|
decision.Reason = AutoBidDuel.Spiegazione(auction.AutoResponsesInARow);
|
||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calcola la velocit� di crescita del prezzo (�/secondo)
|
|
||||||
/// </summary>
|
|
||||||
private double CalculatePriceVelocity(AuctionInfo auction)
|
|
||||||
{
|
|
||||||
if (auction.RecentBids.Count < 5) return 0;
|
|
||||||
|
|
||||||
var recentBids = auction.RecentBids.Take(10).ToList();
|
|
||||||
if (recentBids.Count < 2) return 0;
|
|
||||||
|
|
||||||
var first = recentBids.Last();
|
|
||||||
var last = recentBids.First();
|
|
||||||
|
|
||||||
var timeDiffSeconds = last.Timestamp - first.Timestamp;
|
|
||||||
if (timeDiffSeconds <= 0) return 0;
|
|
||||||
|
|
||||||
var priceDiff = last.Price - first.Price;
|
|
||||||
return (double)priceDiff / timeDiffSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Riconosce un avversario che punta a cadenza fissa.
|
|
||||||
///
|
|
||||||
/// <para><b>Limite di misura, da tenere presente.</b> Le marche temporali dello
|
|
||||||
/// storico di Bidoo sono in <i>secondi interi</i>: le pause fra due puntate sono
|
|
||||||
/// quindi numeri interi, e la loro deviazione standard vale 0 ms (pause tutte
|
|
||||||
/// uguali) oppure almeno 500 ms. La vecchia soglia "deviazione < 50 ms" si
|
|
||||||
/// riduceva percio' a "le ultime pause sono identiche al secondo" - condizione
|
|
||||||
/// comunissima fra utenti normali: rigiocando i dossier raccolti rifiutava fra il
|
|
||||||
/// 4% e il 9% delle puntate, proprio negli istanti in cui il motore avrebbe
|
|
||||||
/// sparato.</para>
|
|
||||||
///
|
|
||||||
/// <para>Ora servono <b>quattro</b> pause tutte uguali e brevi (sotto i 15 s):
|
|
||||||
/// resta un indizio, non una prova, ed e' il motivo per cui l'impostazione che
|
|
||||||
/// la usa nasce spenta.</para>
|
|
||||||
/// </summary>
|
|
||||||
private (bool IsBot, double GapSeconds) DetectBotPattern(AuctionInfo auction, string? lastBidder, string currentUsername)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return (false, 0);
|
|
||||||
|
|
||||||
var userBids = auction.RecentBids
|
// ── 3. Valore atteso appreso ────────────────────────────────────
|
||||||
.Where(b => b.Username.Equals(lastBidder, StringComparison.OrdinalIgnoreCase))
|
//
|
||||||
.OrderByDescending(b => b.Timestamp)
|
// Il modello parla sempre; decide solo quando ha appreso abbastanza aste. La
|
||||||
.Take(5)
|
// probabilità e il valore atteso finiscono comunque sull'asta, per il registro
|
||||||
.ToList();
|
// e per l'interfaccia.
|
||||||
|
double? ev = null;
|
||||||
|
string? spiegazioneEv = null;
|
||||||
|
var pronto = false;
|
||||||
|
|
||||||
if (userBids.Count < 5) return (false, 0);
|
if (settings.LearningGateEnabled && auction.BuyNowPrice is > 0)
|
||||||
|
|
||||||
var gaps = new List<long>();
|
|
||||||
for (var i = 0; i < userBids.Count - 1; i++)
|
|
||||||
gaps.Add(userBids[i].Timestamp - userBids[i + 1].Timestamp);
|
|
||||||
|
|
||||||
// Una pausa nulla vuol dire due puntate nello stesso secondo: e' rumore
|
|
||||||
// dello storico, non una cadenza.
|
|
||||||
if (gaps.Count < 4 || gaps.Any(g => g <= 0 || g > 15)) return (false, 0);
|
|
||||||
|
|
||||||
var isBot = gaps.All(g => g == gaps[0]);
|
|
||||||
return (isBot, gaps[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifica se un utente � esausto (molte puntate, pu� mollare)
|
|
||||||
/// </summary>
|
|
||||||
private (bool ShouldExploit, string Reason) CheckUserExhaustion(AuctionInfo auction, string? lastBidder, string currentUsername)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return (false, "");
|
|
||||||
|
|
||||||
// Verifica se l'utente � un "heavy user" (>50 puntate totali)
|
|
||||||
if (auction.BidderStats.TryGetValue(lastBidder, out var stats))
|
|
||||||
{
|
{
|
||||||
if (stats.BidCount > 50)
|
var prob = Ml.LearningService.PredictUnanswered(auction, state, DateTime.Now, currentUsername);
|
||||||
|
if (prob is { } p)
|
||||||
{
|
{
|
||||||
// Se ci sono pochi altri bidder attivi, pu� essere un buon momento
|
var costo = settings.AverageBidCostEuro * Math.Max(0.1, settings.LearningEvMultiplier);
|
||||||
var activeBidders = auction.BidderStats.Values.Count(b => b.BidCount > 5);
|
var margine = auction.BuyNowPrice.Value - state.Price - 0.01;
|
||||||
if (activeBidders <= 3)
|
var valore = p * margine - costo;
|
||||||
{
|
|
||||||
return (true, $"{lastBidder} ha {stats.BidCount} puntate, potrebbe mollare");
|
auction.LearnedUnansweredProbability = p;
|
||||||
}
|
auction.LearnedExpectedValue = valore;
|
||||||
|
|
||||||
|
pronto = Ml.LearningService.IsReady(settings);
|
||||||
|
ev = valore;
|
||||||
|
spiegazioneEv =
|
||||||
|
$"valore atteso negativo: P(senza risposta) {p:P2} × margine {margine:F2} € " +
|
||||||
|
$"− puntata {costo:F2} € = {valore:+0.000;-0.000} €";
|
||||||
|
|
||||||
|
Ml.LearningService.RecordDecision(auction.Name, state.Price, p, valore,
|
||||||
|
blocked: pronto && valore < 0, ready: pronto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (false, "");
|
// ── 4. Regime di concorrenza ────────────────────────────────────
|
||||||
}
|
//
|
||||||
|
// Finché il modello non è pronto il regime non riceve segnali: resta Calmo e
|
||||||
/// <summary>
|
// l'unica cosa che lo muove è il duello riconosciuto sopra.
|
||||||
/// Calcola probabilit� di puntata basata su competizione e ROI
|
if (pronto && ev is { } valoreAtteso)
|
||||||
/// </summary>
|
|
||||||
private double CalculateBidProbability(AuctionInfo auction, AppSettings settings)
|
|
||||||
{
|
|
||||||
var probability = settings.BaseBidProbability;
|
|
||||||
|
|
||||||
// Riduci probabilit� per ogni bidder attivo oltre la soglia
|
|
||||||
var extraBidders = Math.Max(0, auction.ActiveBiddersCount - settings.CompetitionThreshold);
|
|
||||||
probability -= extraBidders * settings.ProbabilityReductionPerBidder;
|
|
||||||
|
|
||||||
// Riduci per heat metric alto
|
|
||||||
if (auction.HeatMetric > 70)
|
|
||||||
{
|
{
|
||||||
probability -= 0.1;
|
var statoPrima = auction.Regime.StatoAttuale;
|
||||||
|
var consentito = auction.Regime.Osserva(valoreAtteso >= 0);
|
||||||
|
|
||||||
|
if (auction.Regime.StatoAttuale != statoPrima)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[REGIME] {statoPrima} → {auction.Regime.StatoAttuale}: {auction.Regime.Spiegazione()}",
|
||||||
|
AuctionLogLevel.Strategy, AuctionLogCategory.Strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!consentito)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = auction.Regime.StatoAttuale == Ml.CompetitionRegime.Stato.Calmo
|
||||||
|
? spiegazioneEv
|
||||||
|
: auction.Regime.Spiegazione();
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aumenta se abbiamo un buon ROI potenziale
|
return decision;
|
||||||
if (auction.CalculatedValue?.Savings > 0)
|
|
||||||
{
|
|
||||||
probability += 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.Clamp(probability, 0.1, 1.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifica limiti bankroll
|
|
||||||
/// </summary>
|
|
||||||
private BankrollCheckResult CheckBankrollLimits(AuctionInfo auction, AppSettings settings)
|
private BankrollCheckResult CheckBankrollLimits(AuctionInfo auction, AppSettings settings)
|
||||||
{
|
{
|
||||||
var result = new BankrollCheckResult { CanBid = true };
|
var result = new BankrollCheckResult { CanBid = true };
|
||||||
|
|
||||||
// Limite puntate per asta
|
// Limite puntate per asta
|
||||||
var maxPerAuction = auction.MaxBidsOverride ?? settings.MaxBidsPerAuction;
|
var maxPerAuction = auction.MaxBidsOverride ?? settings.MaxBidsPerAuction;
|
||||||
if (maxPerAuction > 0 && auction.SessionBidCount >= maxPerAuction)
|
if (maxPerAuction > 0 && auction.SessionBidCount >= maxPerAuction)
|
||||||
@@ -516,7 +159,7 @@ namespace AutoBidder.Services
|
|||||||
result.Reason = $"Limite puntate per asta raggiunto ({auction.SessionBidCount}/{maxPerAuction})";
|
result.Reason = $"Limite puntate per asta raggiunto ({auction.SessionBidCount}/{maxPerAuction})";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limite puntate per sessione
|
// Limite puntate per sessione
|
||||||
if (settings.MaxBidsPerSession > 0 && _sessionTotalBids >= settings.MaxBidsPerSession)
|
if (settings.MaxBidsPerSession > 0 && _sessionTotalBids >= settings.MaxBidsPerSession)
|
||||||
{
|
{
|
||||||
@@ -524,7 +167,7 @@ namespace AutoBidder.Services
|
|||||||
result.Reason = $"Limite puntate per sessione raggiunto ({_sessionTotalBids}/{settings.MaxBidsPerSession})";
|
result.Reason = $"Limite puntate per sessione raggiunto ({_sessionTotalBids}/{settings.MaxBidsPerSession})";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Budget giornaliero
|
// Budget giornaliero
|
||||||
if (settings.DailyBudgetEuro > 0)
|
if (settings.DailyBudgetEuro > 0)
|
||||||
{
|
{
|
||||||
@@ -532,17 +175,14 @@ namespace AutoBidder.Services
|
|||||||
if (spent >= settings.DailyBudgetEuro)
|
if (spent >= settings.DailyBudgetEuro)
|
||||||
{
|
{
|
||||||
result.CanBid = false;
|
result.CanBid = false;
|
||||||
result.Reason = $"Budget giornaliero esaurito (�{spent:F2}/�{settings.DailyBudgetEuro:F2})";
|
result.Reason = $"Budget giornaliero esaurito (€{spent:F2}/€{settings.DailyBudgetEuro:F2})";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registra una puntata effettuata (per tracking)
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// La sospensione a fascia oraria vale per quest'asta?
|
/// La sospensione a fascia oraria vale per quest'asta?
|
||||||
///
|
///
|
||||||
@@ -563,60 +203,40 @@ namespace AutoBidder.Services
|
|||||||
return settings.QuietHoursEnabled;
|
return settings.QuietHoursEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false)
|
/// <summary>Registra una puntata inviata, riuscita o no.</summary>
|
||||||
|
public void RecordBidAttempt(AuctionInfo auction, bool success)
|
||||||
{
|
{
|
||||||
auction.SessionBidCount++;
|
auction.SessionBidCount++;
|
||||||
_sessionTotalBids++;
|
_sessionTotalBids++;
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
auction.SuccessfulBidCount++;
|
auction.SuccessfulBidCount++;
|
||||||
auction.ConsecutiveCollisions = 0;
|
|
||||||
|
|
||||||
// Da qui si misura quanto tarda la risposta avversaria: e' l'unico punto
|
// Da qui si misura quanto tarda la risposta avversaria: e' l'unico punto
|
||||||
// attraversato sia dal motore dal vivo sia dalla rigiocata sui dossier,
|
// attraversato sia dal motore dal vivo sia dalla rigiocata sui dossier,
|
||||||
// quindi la regola del duello vede le stesse cose in entrambi.
|
// quindi duello e regime vedono le stesse cose in entrambi.
|
||||||
auction.LastMyBidAtUtc = DateTime.UtcNow;
|
auction.LastMyBidAtUtc = DateTime.UtcNow;
|
||||||
|
auction.Regime.PuntataPiazzata();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auction.FailedBidCount++;
|
auction.FailedBidCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collision)
|
|
||||||
{
|
|
||||||
auction.CollisionCount++;
|
|
||||||
auction.ConsecutiveCollisions++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Un ciclo perso perche' la puntata e' arrivata a giochi chiusi.</summary>
|
||||||
/// Registra un ciclo perso perche' la puntata e' arrivata a giochi chiusi.
|
|
||||||
///
|
|
||||||
/// <para>Qui <b>non</b> si tocca <c>ConsecutiveCollisions</c>: chi chiama questo
|
|
||||||
/// metodo ha gia' chiamato <see cref="RecordBidAttempt"/> con <c>collision: true</c>
|
|
||||||
/// sulla stessa puntata, e il contatore veniva percio' incrementato due volte.
|
|
||||||
/// Con la soglia predefinita di tre collisioni bastavano <i>due</i> puntate tardive
|
|
||||||
/// per far scattare il ritiro - e un ritiro di trenta secondi, su cicli da otto o
|
|
||||||
/// dieci, significa perdere l'asta.</para>
|
|
||||||
/// </summary>
|
|
||||||
public void RecordTimerExpired(AuctionInfo auction)
|
public void RecordTimerExpired(AuctionInfo auction)
|
||||||
{
|
{
|
||||||
auction.TimerExpiredCount++;
|
auction.TimerExpiredCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset contatori sessione
|
|
||||||
/// </summary>
|
|
||||||
public void ResetSession()
|
public void ResetSession()
|
||||||
{
|
{
|
||||||
_sessionTotalBids = 0;
|
_sessionTotalBids = 0;
|
||||||
_sessionStartedAt = DateTime.UtcNow;
|
_sessionStartedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ottiene statistiche sessione corrente
|
|
||||||
/// </summary>
|
|
||||||
public SessionStats GetSessionStats()
|
public SessionStats GetSessionStats()
|
||||||
{
|
{
|
||||||
return new SessionStats
|
return new SessionStats
|
||||||
@@ -626,41 +246,19 @@ namespace AutoBidder.Services
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Risultato calcolo timing puntata
|
|
||||||
/// </summary>
|
|
||||||
public class BidTimingResult
|
|
||||||
{
|
|
||||||
public int BaseOffsetMs { get; set; }
|
|
||||||
public int LatencyCompensationMs { get; set; }
|
|
||||||
public int DynamicAdjustmentMs { get; set; }
|
|
||||||
public int JitterMs { get; set; }
|
|
||||||
public int FinalOffsetMs { get; set; }
|
|
||||||
public bool ShouldBid { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decisione se puntare
|
|
||||||
/// </summary>
|
|
||||||
public class BidDecision
|
public class BidDecision
|
||||||
{
|
{
|
||||||
public bool ShouldBid { get; set; }
|
public bool ShouldBid { get; set; }
|
||||||
public string? Reason { get; set; }
|
public string? Reason { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Risultato verifica bankroll
|
|
||||||
/// </summary>
|
|
||||||
public class BankrollCheckResult
|
public class BankrollCheckResult
|
||||||
{
|
{
|
||||||
public bool CanBid { get; set; }
|
public bool CanBid { get; set; }
|
||||||
public string? Reason { get; set; }
|
public string? Reason { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Statistiche sessione
|
|
||||||
/// </summary>
|
|
||||||
public class SessionStats
|
public class SessionStats
|
||||||
{
|
{
|
||||||
public int TotalBids { get; set; }
|
public int TotalBids { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
using AutoBidder.Ml;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Il regime di concorrenza deve avere isteresi nel verso giusto: uscire e' facile, rientrare
|
||||||
|
/// e' difficile, e ogni sondaggio coperto rende il rientro successivo piu' difficile.
|
||||||
|
/// Sbagliare a rientrare costa una puntata; sbagliare a uscire costa un'attesa.
|
||||||
|
/// </summary>
|
||||||
|
public class CompetitionRegimeTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void In_calmo_si_punta_quando_il_valore_atteso_e_positivo()
|
||||||
|
{
|
||||||
|
var r = new CompetitionRegime();
|
||||||
|
Assert.True(r.Osserva(true));
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Calmo, r.StatoAttuale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tre_negativi_di_fila_portano_allo_sfogo_ma_uno_solo_no()
|
||||||
|
{
|
||||||
|
var r = new CompetitionRegime();
|
||||||
|
Assert.False(r.Osserva(false));
|
||||||
|
Assert.False(r.Osserva(false));
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Calmo, r.StatoAttuale);
|
||||||
|
|
||||||
|
Assert.False(r.Osserva(false));
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Sfogo, r.StatoAttuale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_positivo_in_mezzo_azzera_il_conto_dei_negativi()
|
||||||
|
{
|
||||||
|
var r = new CompetitionRegime();
|
||||||
|
r.Osserva(false); r.Osserva(false);
|
||||||
|
r.Osserva(true);
|
||||||
|
r.Osserva(false); r.Osserva(false);
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Calmo, r.StatoAttuale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompetitionRegime InSfogo()
|
||||||
|
{
|
||||||
|
var r = new CompetitionRegime();
|
||||||
|
for (var i = 0; i < CompetitionRegime.NegativiPerSfogo; i++) r.Osserva(false);
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Sfogo, r.StatoAttuale);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dallo_sfogo_si_rientra_solo_dopo_abbastanza_cicli_buoni_di_fila()
|
||||||
|
{
|
||||||
|
var r = InSfogo();
|
||||||
|
|
||||||
|
Assert.False(r.Osserva(true));
|
||||||
|
Assert.False(r.Osserva(true));
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Sfogo, r.StatoAttuale);
|
||||||
|
|
||||||
|
Assert.True(r.Osserva(true)); // terzo: puntata di prova concessa
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Sondaggio, r.StatoAttuale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_ciclo_cattivo_durante_lo_sfogo_azzera_la_serie()
|
||||||
|
{
|
||||||
|
var r = InSfogo();
|
||||||
|
r.Osserva(true); r.Osserva(true);
|
||||||
|
r.Osserva(false);
|
||||||
|
r.Osserva(true); r.Osserva(true);
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Sfogo, r.StatoAttuale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_sondaggio_coperto_subito_raddoppia_la_pazienza()
|
||||||
|
{
|
||||||
|
var r = InSfogo();
|
||||||
|
for (var i = 0; i < CompetitionRegime.PazienzaIniziale; i++) r.Osserva(true);
|
||||||
|
r.PuntataPiazzata();
|
||||||
|
|
||||||
|
r.RispostaAvversaria(6.0); // la firma dell'autopuntata
|
||||||
|
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Sfogo, r.StatoAttuale);
|
||||||
|
Assert.Equal(CompetitionRegime.PazienzaIniziale * 2, r.Pazienza);
|
||||||
|
Assert.Equal(1, r.SondaggiFalliti);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_sondaggio_senza_risposta_riporta_al_calmo()
|
||||||
|
{
|
||||||
|
var r = InSfogo();
|
||||||
|
for (var i = 0; i < CompetitionRegime.PazienzaIniziale; i++) r.Osserva(true);
|
||||||
|
r.PuntataPiazzata();
|
||||||
|
|
||||||
|
r.NessunaRisposta();
|
||||||
|
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Calmo, r.StatoAttuale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Una_risposta_lenta_al_sondaggio_non_e_una_macchina()
|
||||||
|
{
|
||||||
|
var r = InSfogo();
|
||||||
|
for (var i = 0; i < CompetitionRegime.PazienzaIniziale; i++) r.Osserva(true);
|
||||||
|
r.PuntataPiazzata();
|
||||||
|
|
||||||
|
r.RispostaAvversaria(25.0);
|
||||||
|
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Calmo, r.StatoAttuale);
|
||||||
|
Assert.Equal(0, r.SondaggiFalliti);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Durante_il_sondaggio_si_punta_una_volta_sola()
|
||||||
|
{
|
||||||
|
var r = InSfogo();
|
||||||
|
for (var i = 0; i < CompetitionRegime.PazienzaIniziale; i++) r.Osserva(true);
|
||||||
|
r.PuntataPiazzata();
|
||||||
|
|
||||||
|
Assert.False(r.Osserva(true)); // la prova e' in corso: si aspetta l'esito
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_pazienza_ha_un_tetto()
|
||||||
|
{
|
||||||
|
var r = new CompetitionRegime();
|
||||||
|
for (var k = 0; k < 10; k++)
|
||||||
|
{
|
||||||
|
if (r.StatoAttuale == CompetitionRegime.Stato.Calmo)
|
||||||
|
for (var i = 0; i < CompetitionRegime.NegativiPerSfogo; i++) r.Osserva(false);
|
||||||
|
while (r.StatoAttuale == CompetitionRegime.Stato.Sfogo) r.Osserva(true);
|
||||||
|
r.PuntataPiazzata();
|
||||||
|
r.RispostaAvversaria(6.0);
|
||||||
|
}
|
||||||
|
Assert.Equal(CompetitionRegime.PazienzaMassima, r.Pazienza);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void L_autopuntata_riconosciuta_manda_in_sfogo_con_pazienza_doppia()
|
||||||
|
{
|
||||||
|
var r = new CompetitionRegime();
|
||||||
|
r.AutopuntataRiconosciuta();
|
||||||
|
Assert.Equal(CompetitionRegime.Stato.Sfogo, r.StatoAttuale);
|
||||||
|
Assert.True(r.Pazienza >= CompetitionRegime.PazienzaIniziale * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L'anticipo adattivo si muove solo dentro i paletti, sale subito su una puntata
|
||||||
|
/// tardiva e scende piano, e senza campioni usa il predefinito.
|
||||||
|
/// </summary>
|
||||||
|
public class LatencyModelTests
|
||||||
|
{
|
||||||
|
private static AppSettings Paletti(int min = 300, int max = 1500, int predefinito = 500) =>
|
||||||
|
new() { LeadMinMs = min, LeadMaxMs = max, DefaultBidBeforeDeadlineMs = predefinito };
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Senza_campioni_vale_il_predefinito_entro_i_paletti()
|
||||||
|
{
|
||||||
|
LatencyModel.ResetForTests();
|
||||||
|
Assert.Equal(500, LatencyModel.RecommendedLeadMs(Paletti()));
|
||||||
|
Assert.Equal(600, LatencyModel.RecommendedLeadMs(Paletti(min: 600)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void L_anticipo_e_margine_piu_coda_della_latenza()
|
||||||
|
{
|
||||||
|
LatencyModel.ResetForTests(marginMs: 300);
|
||||||
|
for (var i = 0; i < 98; i++) LatencyModel.NotePing(50);
|
||||||
|
LatencyModel.NotePing(400); // la coda: due campioni su cento
|
||||||
|
LatencyModel.NotePing(400);
|
||||||
|
|
||||||
|
// p99 su 100 campioni = il 99-esimo ordinato = 400 -> 300 + 400 = 700
|
||||||
|
Assert.Equal(700, LatencyModel.RecommendedLeadMs(Paletti()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Una_puntata_tardiva_alza_il_margine_subito()
|
||||||
|
{
|
||||||
|
LatencyModel.ResetForTests(marginMs: 300);
|
||||||
|
for (var i = 0; i < 20; i++) LatencyModel.NotePing(50);
|
||||||
|
|
||||||
|
var prima = LatencyModel.RecommendedLeadMs(Paletti());
|
||||||
|
LatencyModel.NoteBid(60, late: true);
|
||||||
|
var dopo = LatencyModel.RecommendedLeadMs(Paletti());
|
||||||
|
|
||||||
|
Assert.Equal(prima + 150, dopo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Molte_puntate_in_tempo_lo_abbassano_piano()
|
||||||
|
{
|
||||||
|
LatencyModel.ResetForTests(marginMs: 600);
|
||||||
|
for (var i = 0; i < 20; i++) LatencyModel.NotePing(50);
|
||||||
|
|
||||||
|
var prima = LatencyModel.RecommendedLeadMs(Paletti());
|
||||||
|
// Stessa latenza dei ping, cosi' cambia solo il margine e non la coda.
|
||||||
|
for (var i = 0; i < 20; i++) LatencyModel.NoteBid(50, late: false);
|
||||||
|
var dopo = LatencyModel.RecommendedLeadMs(Paletti());
|
||||||
|
|
||||||
|
Assert.Equal(prima - 25, dopo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Non_esce_mai_dai_paletti()
|
||||||
|
{
|
||||||
|
LatencyModel.ResetForTests(marginMs: 2000);
|
||||||
|
for (var i = 0; i < 20; i++) LatencyModel.NotePing(900);
|
||||||
|
|
||||||
|
Assert.Equal(1500, LatencyModel.RecommendedLeadMs(Paletti(max: 1500)));
|
||||||
|
|
||||||
|
LatencyModel.ResetForTests(marginMs: 150);
|
||||||
|
for (var i = 0; i < 20; i++) LatencyModel.NotePing(10);
|
||||||
|
Assert.Equal(300, LatencyModel.RecommendedLeadMs(Paletti(min: 300)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,13 +35,9 @@ public class BacktestTests
|
|||||||
/// <summary>Impostazioni con tutte le strategie che potrebbero bloccare messe a tacere.</summary>
|
/// <summary>Impostazioni con tutte le strategie che potrebbero bloccare messe a tacere.</summary>
|
||||||
private static AppSettings Neutral() => new()
|
private static AppSettings Neutral() => new()
|
||||||
{
|
{
|
||||||
AntiBotDetectionEnabled = false,
|
|
||||||
SoftRetreatEnabled = false,
|
|
||||||
ProbabilisticBiddingEnabled = false,
|
|
||||||
CompetitionDetectionEnabled = false,
|
|
||||||
OpponentProfilingEnabled = false,
|
|
||||||
BankrollManagerEnabled = false,
|
BankrollManagerEnabled = false,
|
||||||
PriceVelocityBlockPerSecond = 0
|
AutoBidDuelWithdrawEnabled = false,
|
||||||
|
LearningGateEnabled = false
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Lettura ──────────────────────────────────────────────────────────
|
// ── Lettura ──────────────────────────────────────────────────────────
|
||||||
@@ -195,37 +191,6 @@ public class BacktestTests
|
|||||||
|
|
||||||
// ── Le strategie che bloccano ────────────────────────────────────────
|
// ── Le strategie che bloccano ────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Lanti_bot_acceso_blocca_su_cadenza_fissa_e_spento_no()
|
|
||||||
{
|
|
||||||
// Un avversario che punta esattamente ogni 4 secondi: e' il caso che la vecchia
|
|
||||||
// regola scambiava per automatismo, e sui dossier veri costava fra il 4% e il 9%
|
|
||||||
// delle puntate.
|
|
||||||
var lines = new List<string> { Header };
|
|
||||||
long unix = 1000;
|
|
||||||
for (var i = 0; i < 8; i++)
|
|
||||||
{
|
|
||||||
lines.Add(Bid(i * 0.1, "tizio", unix + i * 4, 1.0 + i * 0.01));
|
|
||||||
}
|
|
||||||
lines.Add(Poll(1.0, 1100, 1100, bidder: "tizio"));
|
|
||||||
lines.Add(SummaryLine);
|
|
||||||
|
|
||||||
var session = DossierReader.Read(lines);
|
|
||||||
|
|
||||||
var acceso = Neutral();
|
|
||||||
acceso.AntiBotDetectionEnabled = true;
|
|
||||||
var bloccato = BacktestRunner.Run(session, new BacktestRunner.Options(1000, acceso));
|
|
||||||
|
|
||||||
Assert.Equal(1, bloccato.Reached);
|
|
||||||
Assert.Equal(0, bloccato.Bids);
|
|
||||||
Assert.Equal(1, bloccato.Blocks["anti-bot"]);
|
|
||||||
|
|
||||||
// Con il predefinito nuovo (spento) la puntata parte.
|
|
||||||
var spento = BacktestRunner.Run(session, new BacktestRunner.Options(1000, Neutral()));
|
|
||||||
Assert.Equal(1, spento.Bids);
|
|
||||||
Assert.Empty(spento.Blocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Il_predefinito_dellapplicazione_non_blocca_le_puntate()
|
public void Il_predefinito_dellapplicazione_non_blocca_le_puntate()
|
||||||
{
|
{
|
||||||
@@ -253,7 +218,7 @@ public class BacktestTests
|
|||||||
{
|
{
|
||||||
new() { Cycles = 10, Reached = 1, Bids = 1, FinalPrice = 1 },
|
new() { Cycles = 10, Reached = 1, Bids = 1, FinalPrice = 1 },
|
||||||
new() { Cycles = 20, Reached = 5, Bids = 3, FinalPrice = 2,
|
new() { Cycles = 20, Reached = 5, Bids = 3, FinalPrice = 2,
|
||||||
Blocks = new Dictionary<string, int>(StringComparer.Ordinal) { ["anti-bot"] = 2 } },
|
Blocks = new Dictionary<string, int>(StringComparer.Ordinal) { ["sfogo"] = 2 } },
|
||||||
new() { Cycles = 30, Reached = 1, Bids = 1, FinalPrice = 3 }
|
new() { Cycles = 30, Reached = 1, Bids = 1, FinalPrice = 3 }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,7 +234,7 @@ public class BacktestTests
|
|||||||
Assert.Equal(2, a.AuctionsWithOneBid);
|
Assert.Equal(2, a.AuctionsWithOneBid);
|
||||||
|
|
||||||
var text = BacktestReport.Format(new[] { a });
|
var text = BacktestReport.Format(new[] { a });
|
||||||
Assert.Contains("anti-bot=2", text);
|
Assert.Contains("sfogo=2", text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La scheda dettagliata di un'asta si legge dal dossier: intestazione in testa e
|
||||||
|
/// riepilogo in coda, senza scorrere il resto. Un dossier senza riepilogo non e' una
|
||||||
|
/// scheda: e' un'asta ancora in corso o interrotta, e non deve entrare nelle statistiche.
|
||||||
|
/// </summary>
|
||||||
|
public class DossierSummaryTests
|
||||||
|
{
|
||||||
|
private const string Header =
|
||||||
|
"""{"type":"header","schema":"autobidder.auction.v1","auctionId":"12345","me":"io","name":"Buono 10","productKey":"buono 10","url":"https://it.bidoo.com/x","addedAt":"2026-09-01T10:00:00+02:00","product":{"buyNowPrice":11,"bidCostEuro":0.2}}""";
|
||||||
|
|
||||||
|
private const string Summary =
|
||||||
|
"""{"type":"summary","closedAt":"10:30:00.000","endedAt":"2026-09-01T10:30:00+02:00","outcome":"Vinta","wonByMe":true,"winner":"io","finalPrice":0.42,"coverage":{"observedFromStart":true,"observedToEnd":true,"complete":true,"firstSeenAt":"2026-09-01T10:00:00+02:00","observedMinutes":30.5},"participation":{"myBids":3,"totalObservedBids":42,"distinctBidders":4,"resets":41,"topBidderShare":0.5,"bidsByUser":{"io":3,"tizio":21,"caio":18}},"value":{"buyNowPrice":11,"shippingCost":1.5,"bidCostEuro":0.2},"network":{"avgPingMs":61,"polls":1200,"pollErrors":2},"engine":{"configuredLeadMs":500,"timerExpiredCount":1,"successfulBids":3,"failedBids":0},"priceSeries":[{"t":0,"price":0.01},{"t":1800,"price":0.42}],"priceVelocityPerMinute":0.0138}""";
|
||||||
|
|
||||||
|
private static string Temp(params string[] lines)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"dossier-{Guid.NewGuid():N}.jsonl");
|
||||||
|
File.WriteAllText(path, string.Join("\n", lines) + "\n");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ricompone_la_scheda_da_intestazione_e_riepilogo()
|
||||||
|
{
|
||||||
|
var path = Temp(Header, """{"t":1,"type":"poll","price":0.01}""", Summary);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var r = AuctionDetailStore.ReadDossier(path);
|
||||||
|
|
||||||
|
Assert.NotNull(r);
|
||||||
|
Assert.Equal("12345", r!.AuctionId);
|
||||||
|
Assert.Equal("Buono 10", r.Name);
|
||||||
|
Assert.Equal("buono 10", r.ProductKey);
|
||||||
|
Assert.True(r.WonByMe);
|
||||||
|
Assert.Equal(0.42, r.FinalPrice, 3);
|
||||||
|
Assert.Equal(11, r.BuyNowPrice);
|
||||||
|
Assert.Equal(1.5, r.ShippingCost);
|
||||||
|
Assert.Equal(3, r.MyBids);
|
||||||
|
Assert.Equal(42, r.TotalObservedBids);
|
||||||
|
Assert.Equal(4, r.DistinctBidders);
|
||||||
|
Assert.Equal(41, r.Resets);
|
||||||
|
Assert.Equal(21, r.BidsByUser["tizio"]);
|
||||||
|
Assert.True(r.IsComplete);
|
||||||
|
Assert.Equal(30.5, r.ObservedMinutes, 2);
|
||||||
|
Assert.Equal(61, r.AveragePingMs);
|
||||||
|
Assert.Equal(1200, r.PollCount);
|
||||||
|
Assert.Equal(500, r.ConfiguredLeadMs);
|
||||||
|
Assert.Equal(2, r.PriceSeries.Count);
|
||||||
|
Assert.Equal(path, r.DossierPath);
|
||||||
|
Assert.Equal(2026, r.EndedAt.Year);
|
||||||
|
Assert.Equal(9, r.EndedAt.Month);
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_dossier_senza_riepilogo_non_e_una_scheda()
|
||||||
|
{
|
||||||
|
var path = Temp(Header, """{"t":1,"type":"poll","price":0.01}""", """{"t":2,"type":"reset","price":0.02}""");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Null(AuctionDetailStore.ReadDossier(path));
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Legge_solo_la_coda_anche_su_un_file_grande()
|
||||||
|
{
|
||||||
|
// Un dossier vero pesa decine di megabyte: qui un megabyte di poll finti in mezzo.
|
||||||
|
var filler = new string[20_000];
|
||||||
|
for (var i = 0; i < filler.Length; i++)
|
||||||
|
filler[i] = $$$"""{"t":{{{i}}},"type":"poll","price":0.01,"timer":9.5,"bidder":"tizio","status":"Running"}""";
|
||||||
|
|
||||||
|
var path = Temp(new[] { Header }.Concat(filler).Append(Summary).ToArray());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var r = AuctionDetailStore.ReadDossier(path);
|
||||||
|
Assert.NotNull(r);
|
||||||
|
Assert.Equal(42, r!.TotalObservedBids);
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_file_che_non_e_un_dossier_da_null_senza_eccezioni()
|
||||||
|
{
|
||||||
|
var path = Temp("questo non e' json", "{}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Null(AuctionDetailStore.ReadDossier(path));
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,19 +53,6 @@ public class RealDossierBacktest
|
|||||||
// vuole mettere alla prova, non una configurazione inventata per l'occasione.
|
// vuole mettere alla prova, non una configurazione inventata per l'occasione.
|
||||||
var settings = new AppSettings();
|
var settings = new AppSettings();
|
||||||
|
|
||||||
// Con AUTOBIDDER_BACKTEST_LEGACY=1 si rimettono i vecchi predefiniti, per vedere
|
|
||||||
// quanto costavano davvero. Serve a rendere visibile la regressione da cui questa
|
|
||||||
// prova difende: senza un confronto, "0 puntate bloccate" non dice se il controllo
|
|
||||||
// funziona o se semplicemente non c'era nulla da bloccare.
|
|
||||||
var legacy = Environment.GetEnvironmentVariable("AUTOBIDDER_BACKTEST_LEGACY") == "1";
|
|
||||||
if (legacy)
|
|
||||||
{
|
|
||||||
settings.AntiBotDetectionEnabled = true;
|
|
||||||
settings.SoftRetreatDurationSeconds = 30;
|
|
||||||
settings.PriceVelocityBlockPerSecond = 0.10;
|
|
||||||
_output.WriteLine("*** vecchi predefiniti (anti-bot acceso, ritiro 30 s, velocita' 0,10 EUR/s) ***");
|
|
||||||
}
|
|
||||||
|
|
||||||
var aggregates = BacktestReport.Run(folder, Leads, settings, username: "", maxFiles: max);
|
var aggregates = BacktestReport.Run(folder, Leads, settings, username: "", maxFiles: max);
|
||||||
var text = BacktestReport.Format(aggregates);
|
var text = BacktestReport.Format(aggregates);
|
||||||
|
|
||||||
@@ -85,25 +72,21 @@ public class RealDossierBacktest
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le due condizioni che la rigiocata deve garantire sui dati veri:
|
// Le due condizioni che la rigiocata deve garantire sui dati veri: il motore non
|
||||||
// il motore non si inceppa, e coi predefiniti nessuna strategia rifiuta puntate.
|
// si inceppa, e ogni blocco ha un motivo riconosciuto. I motivi leciti sono i
|
||||||
|
// paletti (budget, pareggio, tetti) e le tre cose apprese o misurate sul momento
|
||||||
|
// (duello con l'autopuntata, valore atteso, regime). Quello che NON deve esserci
|
||||||
|
// e' un motivo che nessuno riconosce: sarebbe una regola entrata di nascosto.
|
||||||
foreach (var a in aggregates)
|
foreach (var a in aggregates)
|
||||||
{
|
{
|
||||||
Assert.True(a.Reached >= 0 && a.Bids <= a.Reached,
|
Assert.True(a.Reached >= 0 && a.Bids <= a.Reached,
|
||||||
$"conteggi incoerenti con anticipo {a.LeadMs} ms");
|
$"conteggi incoerenti con anticipo {a.LeadMs} ms");
|
||||||
|
|
||||||
if (legacy) continue; // coi vecchi predefiniti i blocchi sono il punto
|
var ignoti = a.Blocks.Where(kv => kv.Key == "altro").ToList();
|
||||||
|
|
||||||
// I blocchi di budget e pareggio sono il comportamento voluto: fermano le
|
Assert.True(ignoti.Count == 0,
|
||||||
// puntate che farebbero perdere soldi, e su un archivio vero devono esserci.
|
$"anticipo {a.LeadMs} ms: puntate bloccate per un motivo non riconosciuto " +
|
||||||
// Quello che NON deve esserci e' un blocco da strategia "difensiva".
|
$"({string.Join(", ", ignoti.Select(kv => $"{kv.Key}={kv.Value}"))})");
|
||||||
var difensivi = a.Blocks
|
|
||||||
.Where(kv => kv.Key is not ("pareggio" or "tetto-spesa" or "tetto-puntate"))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
Assert.True(difensivi.Count == 0,
|
|
||||||
$"con i predefiniti, anticipo {a.LeadMs} ms: puntate bloccate da strategie " +
|
|
||||||
$"difensive ({string.Join(", ", difensivi.Select(kv => $"{kv.Key}={kv.Value}"))})");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legge le schede da una cartella vera di dossier, se indicata con
|
||||||
|
/// AUTOBIDDER_DOSSIER_DIR. Serve a verificare due promesse: che il lettore ricomponga
|
||||||
|
/// le schede da migliaia di file in pochi secondi (legge solo testa e coda), e che
|
||||||
|
/// la seconda lettura, dalla cache, sia istantanea.
|
||||||
|
/// </summary>
|
||||||
|
public class RealDossierSummaries
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public RealDossierSummaries(ITestOutputHelper output) => _output = output;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Legge_le_schede_dai_dossier_veri()
|
||||||
|
{
|
||||||
|
var folder = Environment.GetEnvironmentVariable("AUTOBIDDER_DOSSIER_DIR");
|
||||||
|
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
_output.WriteLine("AUTOBIDDER_DOSSIER_DIR non impostata: prova saltata.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(folder, "*.jsonl");
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
var records = files
|
||||||
|
.Select(AuctionDetailStore.ReadDossier)
|
||||||
|
.Where(r => r != null)
|
||||||
|
.Select(r => r!)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
|
_output.WriteLine($"{files.Length} dossier, {records.Count} con riepilogo, letti in {sw.Elapsed.TotalSeconds:F2} s");
|
||||||
|
_output.WriteLine($"vinte da me: {records.Count(r => r.WonByMe)}, con mie puntate: {records.Count(r => r.MyBids > 0)}, complete: {records.Count(r => r.IsComplete)}");
|
||||||
|
_output.WriteLine($"prima chiusura: {records.Min(r => r.EndedAt):yyyy-MM-dd}, ultima: {records.Max(r => r.EndedAt):yyyy-MM-dd}");
|
||||||
|
_output.WriteLine($"senza prodotto: {records.Count(r => string.IsNullOrEmpty(r.ProductKey))}, senza valore: {records.Count(r => r.BuyNowPrice is null or 0)}, senza serie prezzi: {records.Count(r => r.PriceSeries.Count == 0)}");
|
||||||
|
|
||||||
|
Assert.True(records.Count > 0);
|
||||||
|
Assert.All(records, r => Assert.False(string.IsNullOrEmpty(r.AuctionId)));
|
||||||
|
Assert.True(sw.Elapsed.TotalSeconds < 60, "la lettura di testa e coda non deve scorrere i file");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L'azzeramento tocca solo le voci scelte, e riconosce gli archivi mensili delle
|
||||||
|
/// versioni precedenti dal nome senza confonderli con il resto della cartella.
|
||||||
|
/// </summary>
|
||||||
|
public class StatsWipeTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Toglie_gli_archivi_mensili_e_lascia_il_resto()
|
||||||
|
{
|
||||||
|
var stats = AppPaths.StatsFolder;
|
||||||
|
Directory.CreateDirectory(stats);
|
||||||
|
|
||||||
|
var legacy = Path.Combine(stats, "aste-2020-01.jsonl");
|
||||||
|
var legacyJson = Path.Combine(stats, "aste-2020-02.json");
|
||||||
|
var altro = Path.Combine(stats, $"altro-{Guid.NewGuid():N}.json");
|
||||||
|
|
||||||
|
File.WriteAllText(legacy, "{}\n");
|
||||||
|
File.WriteAllText(legacyJson, "[]");
|
||||||
|
File.WriteAllText(altro, "[]");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var prima = StatsWipe.Measure();
|
||||||
|
Assert.True(prima.LegacyFiles >= 2);
|
||||||
|
|
||||||
|
var report = StatsWipe.Run(new StatsWipe.Options
|
||||||
|
{
|
||||||
|
History = false, ProductStats = false, BidLeadMeasures = false,
|
||||||
|
Exports = false, LegacyArchives = true, Learning = false
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.False(File.Exists(legacy));
|
||||||
|
Assert.False(File.Exists(legacyJson));
|
||||||
|
Assert.True(File.Exists(altro));
|
||||||
|
Assert.Null(report.BackupPath);
|
||||||
|
Assert.Empty(report.Errors);
|
||||||
|
Assert.Contains(report.Steps, s => s.StartsWith("archivi mensili"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var f in new[] { legacy, legacyJson, altro }) if (File.Exists(f)) File.Delete(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Svuota_le_esportazioni_senza_toccare_altro()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(AppPaths.ExportFolder);
|
||||||
|
var export = Path.Combine(AppPaths.ExportFolder, $"storico-{Guid.NewGuid():N}.csv");
|
||||||
|
File.WriteAllText(export, "a;b\n");
|
||||||
|
|
||||||
|
var (files, bytes) = StatsWipe.ClearExports();
|
||||||
|
|
||||||
|
Assert.False(File.Exists(export));
|
||||||
|
Assert.True(files >= 1);
|
||||||
|
Assert.True(bytes >= 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Senza_voci_scelte_non_fa_nulla()
|
||||||
|
{
|
||||||
|
var o = new StatsWipe.Options
|
||||||
|
{
|
||||||
|
History = false, ProductStats = false, BidLeadMeasures = false,
|
||||||
|
Exports = false, LegacyArchives = false, Learning = false
|
||||||
|
};
|
||||||
|
Assert.True(o.Nothing);
|
||||||
|
|
||||||
|
var report = StatsWipe.Run(o);
|
||||||
|
Assert.Empty(report.Steps);
|
||||||
|
Assert.Null(report.BackupPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,81 +2,74 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
|
|
||||||
namespace AutoBidder.Utilities
|
namespace AutoBidder.Utilities
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Schede dettagliate delle aste concluse, una riga per asta.
|
/// Le schede dettagliate delle aste concluse, lette dai dossier.
|
||||||
///
|
///
|
||||||
/// <para>Formato <b>JSON Lines</b>: un oggetto per riga, file separato per mese. La
|
/// <para>Fino a ieri ogni asta chiusa veniva scritta due volte: una riga nel dossier
|
||||||
/// scelta è deliberata — un unico documento JSON andrebbe riletto e riscritto per
|
/// (il riepilogo in coda al file) e la stessa riga, con gli stessi numeri, in un
|
||||||
/// intero a ogni asta conclusa, mentre qui si accoda e basta; e un file che cresce per
|
/// archivio mensile <c>aste-AAAA-MM.jsonl</c>. Cento megabyte di copia, e due fonti
|
||||||
/// mesi diventerebbe scomodo da aprire e impossibile da leggere a pezzi. Ogni riga è
|
/// che potevano divergere. Ora la fonte è una: il dossier. Questa classe ne legge
|
||||||
/// indipendente: strumenti di analisi e fogli di calcolo li leggono in streaming, e un
|
/// intestazione e riepilogo — la prima e l'ultima riga — e ricompone la scheda.</para>
|
||||||
/// file troncato perde al più l'ultima riga invece di diventare illeggibile.</para>
|
///
|
||||||
|
/// <para>Un dossier pesa anche decine di megabyte, ma qui non lo si scorre: si legge
|
||||||
|
/// la testa e la coda del file, poche decine di kilobyte. Seimila dossier si
|
||||||
|
/// rileggono in un paio di secondi, e comunque solo la prima volta — la cache
|
||||||
|
/// ricorda ogni file per dimensione e data, e rilegge soltanto quelli cambiati.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AuctionDetailStore
|
public static class AuctionDetailStore
|
||||||
{
|
{
|
||||||
private static readonly object Sync = new();
|
private static readonly object Sync = new();
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions Options = new()
|
/// <summary>Quanto si legge dalla coda: il riepilogo con 600 punti di prezzo e centinaia di utenti sta in ~40 KB.</summary>
|
||||||
{
|
private const int TailBytes = 256 * 1024;
|
||||||
WriteIndented = false // una riga per record: il formato lo richiede
|
private const int HeadBytes = 8 * 1024;
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>File del mese indicato (predefinito: quello corrente).</summary>
|
private sealed record CacheEntry(long Length, DateTime LastWrite, AuctionDetailRecord? Record);
|
||||||
public static string FileFor(DateTime when) =>
|
|
||||||
Path.Combine(AppPaths.StatsFolder, $"aste-{when:yyyy-MM}.jsonl");
|
|
||||||
|
|
||||||
public static void Append(AuctionDetailRecord record)
|
private static readonly Dictionary<string, CacheEntry> Cache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
{
|
|
||||||
if (record == null) return;
|
|
||||||
|
|
||||||
try
|
/// <summary>Tutte le schede delle aste con dossier concluso, dalla più recente.</summary>
|
||||||
{
|
|
||||||
AppPaths.EnsureFolders();
|
|
||||||
|
|
||||||
var line = JsonSerializer.Serialize(record, Options);
|
|
||||||
|
|
||||||
lock (Sync)
|
|
||||||
{
|
|
||||||
File.AppendAllText(FileFor(record.EndedAt), line + Environment.NewLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* la raccolta dati non deve mai interrompere il monitoraggio */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Tutte le schede registrate, dalla più recente.</summary>
|
|
||||||
public static List<AuctionDetailRecord> LoadAll()
|
public static List<AuctionDetailRecord> LoadAll()
|
||||||
{
|
{
|
||||||
var result = new List<AuctionDetailRecord>();
|
var result = new List<AuctionDetailRecord>();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(AppPaths.StatsFolder)) return result;
|
var folder = AppPaths.AuctionLogFolder;
|
||||||
|
if (!Directory.Exists(folder)) return result;
|
||||||
|
|
||||||
lock (Sync)
|
lock (Sync)
|
||||||
{
|
{
|
||||||
foreach (var file in Directory.GetFiles(AppPaths.StatsFolder, "aste-*.jsonl"))
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
{
|
|
||||||
foreach (var line in File.ReadLines(file))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
||||||
|
|
||||||
try
|
foreach (var file in Directory.EnumerateFiles(folder, "*.jsonl"))
|
||||||
{
|
{
|
||||||
var record = JsonSerializer.Deserialize<AuctionDetailRecord>(line);
|
seen.Add(file);
|
||||||
if (record != null) result.Add(record);
|
|
||||||
}
|
FileInfo info;
|
||||||
catch
|
try { info = new FileInfo(file); } catch { continue; }
|
||||||
{
|
|
||||||
// Una riga corrotta non deve far perdere tutte le altre:
|
if (Cache.TryGetValue(file, out var hit) &&
|
||||||
// è il vantaggio principale di un record per riga.
|
hit.Length == info.Length && hit.LastWrite == info.LastWriteTimeUtc)
|
||||||
}
|
{
|
||||||
|
if (hit.Record != null) result.Add(hit.Record);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var record = ReadDossier(file);
|
||||||
|
Cache[file] = new CacheEntry(info.Length, info.LastWriteTimeUtc, record);
|
||||||
|
if (record != null) result.Add(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File spariti: via dalla cache, o resterebbero per sempre.
|
||||||
|
foreach (var stale in Cache.Keys.Where(k => !seen.Contains(k)).ToList())
|
||||||
|
Cache.Remove(stale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@@ -84,19 +77,181 @@ namespace AutoBidder.Utilities
|
|||||||
return result.OrderByDescending(r => r.EndedAt).ToList();
|
return result.OrderByDescending(r => r.EndedAt).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int Count()
|
public static int Count() => LoadAll().Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La scheda di un singolo dossier, o <c>null</c> se il file non ha un riepilogo
|
||||||
|
/// (asta ancora in corso, o abbandonata a metà).
|
||||||
|
/// </summary>
|
||||||
|
public static AuctionDetailRecord? ReadDossier(string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(AppPaths.StatsFolder)) return 0;
|
string? headerLine, lastLine;
|
||||||
|
|
||||||
lock (Sync)
|
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
{
|
{
|
||||||
return Directory.GetFiles(AppPaths.StatsFolder, "aste-*.jsonl")
|
headerLine = ReadFirstLine(fs);
|
||||||
.Sum(f => File.ReadLines(f).Count(l => !string.IsNullOrWhiteSpace(l)));
|
lastLine = ReadLastLine(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerLine == null || lastLine == null) return null;
|
||||||
|
if (!lastLine.Contains("\"type\":\"summary\"", StringComparison.Ordinal)) return null;
|
||||||
|
|
||||||
|
using var header = JsonDocument.Parse(headerLine);
|
||||||
|
using var summary = JsonDocument.Parse(lastLine);
|
||||||
|
|
||||||
|
return Compose(header.RootElement, summary.RootElement, path);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lettura di testa e coda ──────────────────────────────────────
|
||||||
|
|
||||||
|
private static string? ReadFirstLine(FileStream fs)
|
||||||
|
{
|
||||||
|
fs.Seek(0, SeekOrigin.Begin);
|
||||||
|
var buffer = new byte[Math.Min(HeadBytes, fs.Length)];
|
||||||
|
var read = fs.Read(buffer, 0, buffer.Length);
|
||||||
|
var text = Encoding.UTF8.GetString(buffer, 0, read).TrimStart('');
|
||||||
|
var nl = text.IndexOf('\n');
|
||||||
|
var line = nl >= 0 ? text[..nl] : text;
|
||||||
|
return line.Contains("\"type\":\"header\"", StringComparison.Ordinal) ? line.TrimEnd('\r') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ReadLastLine(FileStream fs)
|
||||||
|
{
|
||||||
|
var take = (int)Math.Min(TailBytes, fs.Length);
|
||||||
|
fs.Seek(-take, SeekOrigin.End);
|
||||||
|
var buffer = new byte[take];
|
||||||
|
var read = fs.Read(buffer, 0, take);
|
||||||
|
var text = Encoding.UTF8.GetString(buffer, 0, read).TrimEnd('\r', '\n', ' ');
|
||||||
|
|
||||||
|
var nl = text.LastIndexOf('\n');
|
||||||
|
// Se non c'è un a-capo nella coda letta, la riga è più lunga della finestra
|
||||||
|
// (o il file è di una riga sola): si tiene solo se comincia con la graffa.
|
||||||
|
var line = nl >= 0 ? text[(nl + 1)..] : text;
|
||||||
|
line = line.TrimEnd('\r');
|
||||||
|
return line.StartsWith('{') ? line : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ricomposizione ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static AuctionDetailRecord Compose(JsonElement h, JsonElement s, string path)
|
||||||
|
{
|
||||||
|
var r = new AuctionDetailRecord
|
||||||
|
{
|
||||||
|
AuctionId = Str(h, "auctionId"),
|
||||||
|
Name = Str(h, "name"),
|
||||||
|
ProductKey = Str(h, "productKey"),
|
||||||
|
Url = Str(h, "url"),
|
||||||
|
DossierPath = path,
|
||||||
|
|
||||||
|
Outcome = Str(s, "outcome"),
|
||||||
|
WonByMe = Bool(s, "wonByMe"),
|
||||||
|
Winner = Str(s, "winner"),
|
||||||
|
FinalPrice = Num(s, "finalPrice"),
|
||||||
|
EndedAt = Date(s, "endedAt") ?? Date(s, "closedAt") ?? DateTime.MinValue,
|
||||||
|
PriceVelocityPerMinute = Num(s, "priceVelocityPerMinute")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (h.TryGetProperty("product", out var product))
|
||||||
|
{
|
||||||
|
r.BuyNowPrice = NullableNum(product, "buyNowPrice");
|
||||||
|
r.BidCostEuro = Num(product, "bidCostEuro", 0.20);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.TryGetProperty("coverage", out var cov))
|
||||||
|
{
|
||||||
|
r.ObservedFromStart = Bool(cov, "observedFromStart");
|
||||||
|
r.ObservedToEnd = Bool(cov, "observedToEnd");
|
||||||
|
r.FirstSeenAt = Date(cov, "firstSeenAt") ?? Date(h, "addedAt") ?? r.EndedAt;
|
||||||
|
r.ObservedMinutes = Num(cov, "observedMinutes");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
r.FirstSeenAt = Date(h, "addedAt") ?? r.EndedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.TryGetProperty("participation", out var part))
|
||||||
|
{
|
||||||
|
r.MyBids = Int(part, "myBids");
|
||||||
|
r.TotalObservedBids = Int(part, "totalObservedBids");
|
||||||
|
r.DistinctBidders = Int(part, "distinctBidders");
|
||||||
|
r.Resets = Int(part, "resets");
|
||||||
|
r.TopBidderShare = Num(part, "topBidderShare");
|
||||||
|
|
||||||
|
if (part.TryGetProperty("bidsByUser", out var byUser) && byUser.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
foreach (var p in byUser.EnumerateObject())
|
||||||
|
if (p.Value.TryGetInt32(out var n)) r.BidsByUser[p.Name] = n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { return 0; }
|
|
||||||
|
if (s.TryGetProperty("value", out var value))
|
||||||
|
{
|
||||||
|
r.BuyNowPrice = NullableNum(value, "buyNowPrice") ?? r.BuyNowPrice;
|
||||||
|
r.ShippingCost = NullableNum(value, "shippingCost");
|
||||||
|
var costo = NullableNum(value, "bidCostEuro");
|
||||||
|
if (costo is > 0) r.BidCostEuro = costo.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.TryGetProperty("network", out var net))
|
||||||
|
{
|
||||||
|
r.AveragePingMs = Num(net, "avgPingMs");
|
||||||
|
r.PollCount = Long(net, "polls");
|
||||||
|
r.PollErrors = Long(net, "pollErrors");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.TryGetProperty("engine", out var eng))
|
||||||
|
{
|
||||||
|
r.ConfiguredLeadMs = Int(eng, "configuredLeadMs");
|
||||||
|
r.TimerExpiredCount = Int(eng, "timerExpiredCount");
|
||||||
|
r.SuccessfulBids = Int(eng, "successfulBids");
|
||||||
|
r.FailedBids = Int(eng, "failedBids");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.TryGetProperty("priceSeries", out var series) && series.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var p in series.EnumerateArray())
|
||||||
|
r.PriceSeries.Add(new PricePoint { T = Num(p, "t"), Price = Num(p, "price") });
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Str(JsonElement e, string name) =>
|
||||||
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : "";
|
||||||
|
|
||||||
|
private static bool Bool(JsonElement e, string name) =>
|
||||||
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.True;
|
||||||
|
|
||||||
|
private static double Num(JsonElement e, string name, double fallback = 0) =>
|
||||||
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetDouble() : fallback;
|
||||||
|
|
||||||
|
private static double? NullableNum(JsonElement e, string name) =>
|
||||||
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetDouble() : null;
|
||||||
|
|
||||||
|
private static int Int(JsonElement e, string name) =>
|
||||||
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var n) ? n : 0;
|
||||||
|
|
||||||
|
private static long Long(JsonElement e, string name) =>
|
||||||
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n) ? n : 0;
|
||||||
|
|
||||||
|
private static DateTime? Date(JsonElement e, string name) =>
|
||||||
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String &&
|
||||||
|
DateTime.TryParse(v.GetString(), System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
System.Globalization.DateTimeStyles.RoundtripKind, out var d)
|
||||||
|
? d
|
||||||
|
: null;
|
||||||
|
|
||||||
|
/// <summary>Solo per i test: dimentica i file già letti.</summary>
|
||||||
|
public static void ResetCacheForTests()
|
||||||
|
{
|
||||||
|
lock (Sync) Cache.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,12 +233,12 @@ namespace AutoBidder.Utilities
|
|||||||
maxBids = auction.MaxClicks,
|
maxBids = auction.MaxClicks,
|
||||||
minResets = auction.MinResets,
|
minResets = auction.MinResets,
|
||||||
maxResets = auction.MaxResets,
|
maxResets = auction.MaxResets,
|
||||||
pollCriticalMs = settings.PollIntervalCriticalMs,
|
bidLeadIsManual = auction.BidLeadIsManual,
|
||||||
criticalWindowMs = settings.CriticalWindowMs,
|
adaptiveLead = settings.AdaptiveLeadEnabled,
|
||||||
|
leadMinMs = settings.LeadMinMs,
|
||||||
|
leadMaxMs = settings.LeadMaxMs,
|
||||||
valueCheckEnabled = settings.ValueCheckEnabled,
|
valueCheckEnabled = settings.ValueCheckEnabled,
|
||||||
minSavingsPercentage = settings.MinSavingsPercentage,
|
minSavingsPercentage = settings.MinSavingsPercentage,
|
||||||
antiBotEnabled = settings.AntiBotDetectionEnabled,
|
|
||||||
competitionEnabled = settings.CompetitionDetectionEnabled,
|
|
||||||
rawPollsIncluded = settings.DossierIncludeRawPolls
|
rawPollsIncluded = settings.DossierIncludeRawPolls
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -324,8 +324,7 @@ namespace AutoBidder.Utilities
|
|||||||
configuredLeadMs = detail.ConfiguredLeadMs,
|
configuredLeadMs = detail.ConfiguredLeadMs,
|
||||||
timerExpiredCount = auction.TimerExpiredCount,
|
timerExpiredCount = auction.TimerExpiredCount,
|
||||||
successfulBids = auction.SuccessfulBidCount,
|
successfulBids = auction.SuccessfulBidCount,
|
||||||
failedBids = auction.FailedBidCount,
|
failedBids = auction.FailedBidCount
|
||||||
collisions = auction.CollisionCount
|
|
||||||
},
|
},
|
||||||
|
|
||||||
priceSeries = detail.PriceSeries.Select(p => new { t = Math.Round(p.T, 2), price = p.Price }),
|
priceSeries = detail.PriceSeries.Select(p => new { t = Math.Round(p.T, 2), price = p.Price }),
|
||||||
|
|||||||
@@ -279,6 +279,21 @@ namespace AutoBidder.Utilities
|
|||||||
return summaries.Count;
|
return summaries.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Azzera i numeri di tutte le schede tenendo le opzioni scelte nella scheda
|
||||||
|
/// Prodotti: si perde la statistica, non la configurazione.
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearStatistics()
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
_cache = new StoreData();
|
||||||
|
Save(_cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncOptionsFromWatchList();
|
||||||
|
}
|
||||||
|
|
||||||
private static void SyncOptions(ProductCard card)
|
private static void SyncOptions(ProductCard card)
|
||||||
{
|
{
|
||||||
var product = WatchedProductsStore.GetAll()
|
var product = WatchedProductsStore.GetAll()
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ namespace AutoBidder.Utilities
|
|||||||
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;
|
||||||
public int DefaultMinResets { get; set; } = 0;
|
|
||||||
public int DefaultMaxResets { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mostra avviso quando una puntata arriva troppo tardi (timer scaduto).
|
/// Mostra avviso quando una puntata arriva troppo tardi (timer scaduto).
|
||||||
@@ -73,25 +71,6 @@ namespace AutoBidder.Utilities
|
|||||||
// la scadenza si avvicina: un'asta a otto minuti non ha bisogno di quattro
|
// la scadenza si avvicina: un'asta a otto minuti non ha bisogno di quattro
|
||||||
// chiamate al secondo, una a tre secondi sì.
|
// chiamate al secondo, una a tre secondi sì.
|
||||||
|
|
||||||
/// <summary>Cadenza polling oltre i 60 s dalla scadenza. Default: 2000 ms.</summary>
|
|
||||||
public int PollIntervalFarMs { get; set; } = 2000;
|
|
||||||
|
|
||||||
/// <summary>Cadenza polling tra 10 e 60 s dalla scadenza. Default: 900 ms.</summary>
|
|
||||||
public int PollIntervalMidMs { get; set; } = 900;
|
|
||||||
|
|
||||||
/// <summary>Cadenza polling sotto i 10 s dalla scadenza. Default: 400 ms.</summary>
|
|
||||||
public int PollIntervalNearMs { get; set; } = 400;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cadenza polling nella finestra critica, solo per le aste in stato Attiva.
|
|
||||||
/// In sola osservazione non c'è puntata da azzeccare e si risparmiano chiamate.
|
|
||||||
/// Default: 220 ms.
|
|
||||||
/// </summary>
|
|
||||||
public int PollIntervalCriticalMs { get; set; } = 220;
|
|
||||||
|
|
||||||
/// <summary>Ampiezza della finestra critica prima della scadenza. Default: 4000 ms.</summary>
|
|
||||||
public int CriticalWindowMs { get; set; } = 4000;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
@@ -147,12 +126,6 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool BidLeadTrackingEnabled { get; set; } = true;
|
public bool BidLeadTrackingEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Propone (non applica) una correzione dell'anticipo quando i dati raccolti
|
|
||||||
/// mostrano uno scarto sistematico. Default: true.
|
|
||||||
/// </summary>
|
|
||||||
public bool BidLeadSuggestionsEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>Misure necessarie prima di azzardare una proposta. Default: 15.</summary>
|
/// <summary>Misure necessarie prima di azzardare una proposta. Default: 15.</summary>
|
||||||
public int BidLeadMinSamples { get; set; } = 15;
|
public int BidLeadMinSamples { get; set; } = 15;
|
||||||
|
|
||||||
@@ -217,12 +190,6 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int LogRetentionDays { get; set; } = 90;
|
public int LogRetentionDays { get; set; } = 90;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registra una scheda dettagliata per ogni asta conclusa (serie dei prezzi,
|
|
||||||
/// puntate per utente, durata). È la materia prima per migliorare le strategie.
|
|
||||||
/// </summary>
|
|
||||||
public bool DetailedStatsEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// CATALOGO — cache
|
// CATALOGO — cache
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -401,31 +368,6 @@ namespace AutoBidder.Utilities
|
|||||||
// IMPOSTAZIONI DATABASE
|
// IMPOSTAZIONI DATABASE
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita il salvataggio automatico delle aste completate nel database.
|
|
||||||
/// Default: true (consigliato per statistiche)
|
|
||||||
/// </summary>
|
|
||||||
public bool DatabaseAutoSaveEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Esegue pulizia automatica duplicati all'avvio dell'applicazione.
|
|
||||||
/// Default: true (consigliato per mantenere database pulito)
|
|
||||||
/// </summary>
|
|
||||||
public bool DatabaseAutoCleanupDuplicates { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Esegue pulizia automatica record incompleti all'avvio.
|
|
||||||
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
|
|
||||||
/// </summary>
|
|
||||||
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Numero massimo di giorni da mantenere nei risultati aste.
|
|
||||||
/// Record più vecchi vengono eliminati automaticamente.
|
|
||||||
/// Default: 180 (6 mesi), 0 = disabilitato
|
|
||||||
/// </summary>
|
|
||||||
public int DatabaseMaxRetentionDays { get; set; } = 180;
|
|
||||||
|
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
// STRATEGIE AVANZATE DI PUNTATA
|
// STRATEGIE AVANZATE DI PUNTATA
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
@@ -449,24 +391,6 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool LogStrategyDecisions { get; set; } = true;
|
public bool LogStrategyDecisions { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Log calcoli valore prodotto [VALUE]
|
|
||||||
/// Default: false (attiva per debug)
|
|
||||||
/// </summary>
|
|
||||||
public bool LogValueCalculations { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Log rilevamento competizione e heat [COMPETITION]
|
|
||||||
/// Default: false
|
|
||||||
/// </summary>
|
|
||||||
public bool LogCompetition { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Log timing e polling (molto verbose!) [TIMING]
|
|
||||||
/// Default: false (attiva solo per debug timing)
|
|
||||||
/// </summary>
|
|
||||||
public bool LogTiming { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log errori e warning [ERROR/WARN]
|
/// Log errori e warning [ERROR/WARN]
|
||||||
/// Default: true
|
/// Default: true
|
||||||
@@ -494,45 +418,9 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool LogAuctionStatus { get; set; } = true;
|
public bool LogAuctionStatus { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Log profiling avversari [OPPONENT]
|
|
||||||
/// Default: false
|
|
||||||
/// </summary>
|
|
||||||
public bool LogOpponentProfiling { get; set; } = false;
|
|
||||||
|
|
||||||
// 🎯 STRATEGIE SEMPLIFICATE
|
// 🎯 STRATEGIE SEMPLIFICATE
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Entry Point: Usato SOLO per calcolare i limiti consigliati (70% del MaxPrice storico).
|
|
||||||
/// NON blocca le puntate! I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
|
||||||
/// Default: true (per calcolo limiti consigliati)
|
|
||||||
/// </summary>
|
|
||||||
public bool EntryPointEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Anti-bot: rinuncia al ciclo quando l'ultimo puntatore punta a cadenza fissa.
|
|
||||||
///
|
|
||||||
/// <para><b>Predefinito cambiato a false.</b> Le marche temporali di Bidoo sono in
|
|
||||||
/// secondi interi, quindi la regola non puo' davvero distinguere un automatismo da
|
|
||||||
/// una persona regolare: si riduce a "le ultime pause dell'avversario sono identiche
|
|
||||||
/// al secondo", cosa comunissima. Rigiocando i dossier con <c>backtest.ps1</c>
|
|
||||||
/// rifiutava fra il <b>4% e il 9%</b> delle puntate, a seconda dell'anticipo, e la
|
|
||||||
/// premessa era comunque rovesciata: un avversario a cadenza fissa punta con secondi
|
|
||||||
/// di anticipo, quindi e' il piu' facile da battere aspettando l'ultimo istante.</para>
|
|
||||||
///
|
|
||||||
/// <para>Per rivedere il confronto con i propri dati:
|
|
||||||
/// <c>$env:AUTOBIDDER_BACKTEST_LEGACY=1</c> prima di <c>backtest.ps1</c>.</para>
|
|
||||||
/// </summary>
|
|
||||||
public bool AntiBotDetectionEnabled { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// User Exhaustion: Sfrutta utenti stanchi (oltre 50 puntate)
|
|
||||||
/// quando ci sono pochi altri bidder attivi.
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool UserExhaustionEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
// 🎯 CONTROLLO CONVENIENZA PRODOTTO
|
// 🎯 CONTROLLO CONVENIENZA PRODOTTO
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -552,14 +440,6 @@ namespace AutoBidder.Utilities
|
|||||||
/// Default: -5 (permetti fino al 5% di perdita)
|
/// Default: -5 (permetti fino al 5% di perdita)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double MinSavingsPercentage { get; set; } = -5.0;
|
public double MinSavingsPercentage { get; set; } = -5.0;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita il controllo anti-collisione hardcoded.
|
|
||||||
/// Se attivo, blocca le puntate quando ci sono 3+ bidder attivi negli ultimi 10 secondi.
|
|
||||||
/// ATTENZIONE: Questo controllo può far perdere aste competitive!
|
|
||||||
/// Default: false (DISABILITATO - non blocca mai)
|
|
||||||
/// </summary>
|
|
||||||
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
|
|
||||||
|
|
||||||
// ── Fascia oraria sospesa ────────────────────────────────────────
|
// ── Fascia oraria sospesa ────────────────────────────────────────
|
||||||
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
|
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
|
||||||
@@ -594,6 +474,29 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int LearningBootstrapSecondsPerStart { get; set; } = 120;
|
public int LearningBootstrapSecondsPerStart { get; set; } = 120;
|
||||||
|
|
||||||
|
// ── Anticipo adattivo ────────────────────────────────────────────
|
||||||
|
// Vedi Ml/LatencyModel: l'anticipo lo decide la rete di questa sessione, dentro
|
||||||
|
// i paletti qui sotto. Un anticipo scritto a mano su una singola asta vince sempre.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lasciar decidere l'anticipo al modello di latenza (margine + coda del ping).
|
||||||
|
/// Spento, vale <see cref="DefaultBidBeforeDeadlineMs"/> per tutte le aste senza
|
||||||
|
/// un anticipo proprio.
|
||||||
|
/// </summary>
|
||||||
|
public bool AdaptiveLeadEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anticipo minimo che il modello può scegliere. Sotto i 300 ms un picco di rete
|
||||||
|
/// fa arrivare la puntata a giochi chiusi: il ping tocca 444 ms nel p99,9.
|
||||||
|
/// </summary>
|
||||||
|
public int LeadMinMs { get; set; } = 300;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anticipo massimo che il modello può scegliere. Oltre 1500 ms si regala
|
||||||
|
/// più di un secondo agli avversari a ogni ciclo, e il costo per vincere sale.
|
||||||
|
/// </summary>
|
||||||
|
public int LeadMaxMs { get; set; } = 1500;
|
||||||
|
|
||||||
// ── Rimozione automatica delle aste concluse ─────────────────────
|
// ── Rimozione automatica delle aste concluse ─────────────────────
|
||||||
// Vedi FinishedAuctionCleanup: si toglie di serie, si trattiene cio' su cui c'e'
|
// Vedi FinishedAuctionCleanup: si toglie di serie, si trattiene cio' su cui c'e'
|
||||||
// stato un esborso.
|
// stato un esborso.
|
||||||
@@ -650,142 +553,18 @@ namespace AutoBidder.Utilities
|
|||||||
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||||||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita rilevamento competizione e heat metric.
|
|
||||||
/// Conta bidder attivi e collisioni per determinare il "calore" dell'asta.
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool CompetitionDetectionEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finestra temporale in secondi per contare bidder attivi.
|
|
||||||
/// Default: 30 (ultimi 30 secondi)
|
|
||||||
/// </summary>
|
|
||||||
public int CompetitionWindowSeconds { get; set; } = 30;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Numero minimo di bidder attivi per considerare l'asta "affollata".
|
|
||||||
/// Se >= a questa soglia, applica logica di evitamento.
|
|
||||||
/// Default: 3
|
|
||||||
/// </summary>
|
|
||||||
public int CompetitionThreshold { get; set; } = 3;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita auto-pausa per aste troppo competitive.
|
|
||||||
/// Default: false (solo warning, non pausa automatica)
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoPauseHotAuctions { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Soglia heat metric per auto-pausa (0-100).
|
|
||||||
/// Default: 80 (pausa se heat >= 80%)
|
|
||||||
/// </summary>
|
|
||||||
public int HeatThresholdForPause { get; set; } = 80;
|
|
||||||
|
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
// SOFT RETREAT E COLLISION MANAGEMENT
|
// SOFT RETREAT E COLLISION MANAGEMENT
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita soft retreat automatico dopo N collisioni consecutive.
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool SoftRetreatEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Numero di collisioni consecutive per attivare soft retreat.
|
|
||||||
/// Default: 3
|
|
||||||
/// </summary>
|
|
||||||
public int SoftRetreatAfterCollisions { get; set; } = 3;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Durata del ritiro, in secondi.
|
|
||||||
///
|
|
||||||
/// <para>Ridotta da 30 a 12: il timer delle aste osservate si azzera ogni 8-12
|
|
||||||
/// secondi, quindi trenta secondi di pausa sono tre cicli interi — nella pratica
|
|
||||||
/// l'asta è persa. Dodici secondi bastano a spezzare una serie di collisioni
|
|
||||||
/// senza consegnare la partita.</para>
|
|
||||||
/// </summary>
|
|
||||||
public int SoftRetreatDurationSeconds { get; set; } = 12;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Blocca la puntata se il prezzo sale più in fretta di tanti euro al secondo.
|
|
||||||
/// 0 = controllo spento (predefinito).
|
|
||||||
///
|
|
||||||
/// <para>Prima era una costante nel codice fissata a 0,10 €/s, cioè dieci puntate
|
|
||||||
/// al secondo: su oltre 40.000 valutazioni riprese dai dossier non è mai scattata,
|
|
||||||
/// e la velocità massima mai osservata è 0,016 €/s. Se lo si vuole usare davvero,
|
|
||||||
/// un valore sensato sta intorno a 0,01-0,02 €/s.</para>
|
|
||||||
/// </summary>
|
|
||||||
public double PriceVelocityBlockPerSecond { get; set; } = 0;
|
|
||||||
|
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
// PROBABILISTIC BIDDING
|
// PROBABILISTIC BIDDING
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita policy di puntata probabilistica.
|
|
||||||
/// Decide se puntare con probabilità p basata su competizione e ROI.
|
|
||||||
/// Default: false (richiede tuning)
|
|
||||||
/// </summary>
|
|
||||||
public bool ProbabilisticBiddingEnabled { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Probabilità base di puntata (0.0 - 1.0).
|
|
||||||
/// Default: 0.8 (80%)
|
|
||||||
/// </summary>
|
|
||||||
public double BaseBidProbability { get; set; } = 0.8;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fattore di riduzione probabilità per ogni bidder attivo extra.
|
|
||||||
/// Default: 0.1 (riduce del 10% per ogni bidder oltre la soglia)
|
|
||||||
/// </summary>
|
|
||||||
public double ProbabilityReductionPerBidder { get; set; } = 0.1;
|
|
||||||
|
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
// OPPONENT PROFILING
|
// OPPONENT PROFILING
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita profiling degli avversari.
|
|
||||||
/// Identifica utenti aggressivi e applica regole specifiche.
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool OpponentProfilingEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Soglia puntate per considerare un utente "aggressivo".
|
|
||||||
/// Default: 10 (se un utente ha fatto >= 10 puntate in un'asta)
|
|
||||||
/// </summary>
|
|
||||||
public int AggressiveBidderThreshold { get; set; } = 10;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dimensione finestra scorrevole per analisi bidder aggressivi.
|
|
||||||
/// Analizza le ultime N puntate invece del conteggio totale.
|
|
||||||
/// Default: 30 (ultime 30 puntate)
|
|
||||||
/// </summary>
|
|
||||||
public int AggressiveBidderWindowSize { get; set; } = 30;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Soglia percentuale per considerare un utente "aggressivo".
|
|
||||||
/// Se un utente ha più di X% delle puntate nella finestra, è aggressivo.
|
|
||||||
/// Default: 40 (40% delle puntate)
|
|
||||||
/// </summary>
|
|
||||||
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dimensione finestra per rilevamento situazioni di duello.
|
|
||||||
/// Default: 20 (ultime 20 puntate)
|
|
||||||
/// </summary>
|
|
||||||
public int DuelDetectionWindowSize { get; set; } = 20;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Azione da intraprendere con bidder aggressivi.
|
|
||||||
/// "Avoid" = evita l'asta, "Compete" = continua normalmente, "Outbid" = punta più aggressivamente
|
|
||||||
/// Default: "Compete" (cambiato da Avoid per essere meno restrittivo)
|
|
||||||
/// </summary>
|
|
||||||
public string AggressiveBidderAction { get; set; } = "Compete";
|
|
||||||
|
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
// BANKROLL & SAFETY MANAGER
|
// BANKROLL & SAFETY MANAGER
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
@@ -840,19 +619,6 @@ namespace AutoBidder.Utilities
|
|||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
// LOGGING AVANZATO
|
// LOGGING AVANZATO
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Abilita logging avanzato con metriche dettagliate.
|
|
||||||
/// Include: collisioni, timer scaduto, latenza, heat metric.
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool AdvancedLoggingEnabled { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Salva metriche per ogni puntata nel database.
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool SaveBidMetricsToDatabase { get; set; } = true;
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// INTERFACCIA
|
// INTERFACCIA
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AutoBidder.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Azzera le statistiche registrate, una voce alla volta e solo quelle scelte.
|
||||||
|
///
|
||||||
|
/// <para>Ogni voce è un archivio con un padrone diverso, e ognuno si svuota nel modo
|
||||||
|
/// suo: lo storico passa dalla copia di sicurezza, le statistiche per prodotto tengono
|
||||||
|
/// le opzioni e azzerano i numeri, l'apprendimento dimentica anche in memoria — se no
|
||||||
|
/// il modello ancora vivo riscriverebbe il file al primo salvataggio. I dossier delle
|
||||||
|
/// aste non si toccano da qui: sono i dati grezzi da cui tutto il resto si
|
||||||
|
/// ricostruisce.</para>
|
||||||
|
///
|
||||||
|
/// <para><see cref="Measure"/> pesa ogni voce prima: cancellare è irreversibile, e
|
||||||
|
/// dire quanto sparisce è il minimo per una scelta informata.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class StatsWipe
|
||||||
|
{
|
||||||
|
public sealed class Options
|
||||||
|
{
|
||||||
|
public bool History { get; set; } = true;
|
||||||
|
public bool ProductStats { get; set; } = true;
|
||||||
|
public bool BidLeadMeasures { get; set; } = true;
|
||||||
|
public bool Exports { get; set; } = true;
|
||||||
|
public bool LegacyArchives { get; set; } = true;
|
||||||
|
public bool Learning { get; set; }
|
||||||
|
|
||||||
|
public bool Nothing => !(History || ProductStats || BidLeadMeasures || Exports || LegacyArchives || Learning);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct Sizes(
|
||||||
|
int HistoryAuctions, long HistoryBytes,
|
||||||
|
long ProductsBytes,
|
||||||
|
int BidLeadSamples, long BidLeadBytes,
|
||||||
|
int ExportFiles, long ExportBytes,
|
||||||
|
int LegacyFiles, long LegacyBytes,
|
||||||
|
long LearningBytes);
|
||||||
|
|
||||||
|
public sealed class Report
|
||||||
|
{
|
||||||
|
public string? BackupPath { get; init; }
|
||||||
|
public int FilesDeleted { get; set; }
|
||||||
|
public long BytesFreed { get; set; }
|
||||||
|
public List<string> Steps { get; } = new();
|
||||||
|
public List<string> Errors { get; } = new();
|
||||||
|
|
||||||
|
public string Summary =>
|
||||||
|
string.Join("; ", Steps) +
|
||||||
|
(Errors.Count > 0 ? $". Problemi: {string.Join("; ", Errors)}" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string LearningFolder => Path.Combine(AppPaths.StatsFolder, "Apprendimento");
|
||||||
|
|
||||||
|
// ── Misura ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static Sizes Measure()
|
||||||
|
{
|
||||||
|
int aste = 0, misure = 0;
|
||||||
|
try { aste = CompletedAuctionsStore.LoadAll().Count; } catch { }
|
||||||
|
try { misure = BidLeadStats.All().Count; } catch { }
|
||||||
|
|
||||||
|
var export = ListFiles(AppPaths.ExportFolder, "*");
|
||||||
|
var legacy = LegacyFiles();
|
||||||
|
var learning = ListFiles(LearningFolder, "*");
|
||||||
|
|
||||||
|
return new Sizes(
|
||||||
|
aste, SizeOf(CompletedAuctionsStore.StorePath),
|
||||||
|
SizeOf(AppPaths.ProductsFile),
|
||||||
|
misure, SizeOf(AppPaths.BidLeadStatsFile),
|
||||||
|
export.Count, export.Sum(SizeOf),
|
||||||
|
legacy.Count, legacy.Sum(SizeOf),
|
||||||
|
learning.Sum(SizeOf));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Azzeramento ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static Report Run(Options o)
|
||||||
|
{
|
||||||
|
string? backup = null;
|
||||||
|
|
||||||
|
if (o.History)
|
||||||
|
{
|
||||||
|
// La copia prima di tutto: se fallisce, non si cancella niente.
|
||||||
|
backup = CompletedAuctionsStore.BackupNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
var report = new Report { BackupPath = backup };
|
||||||
|
|
||||||
|
if (o.History)
|
||||||
|
{
|
||||||
|
Try(report, "storico", () =>
|
||||||
|
{
|
||||||
|
report.BytesFreed += SizeOf(CompletedAuctionsStore.StorePath);
|
||||||
|
CompletedAuctionsStore.Clear();
|
||||||
|
report.Steps.Add($"storico svuotato (copia in {backup})");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.ProductStats)
|
||||||
|
{
|
||||||
|
Try(report, "statistiche prodotto", () =>
|
||||||
|
{
|
||||||
|
ProductStatsStore.ClearStatistics();
|
||||||
|
report.Steps.Add("statistiche per prodotto azzerate (opzioni conservate)");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.BidLeadMeasures)
|
||||||
|
{
|
||||||
|
Try(report, "misure anticipo", () =>
|
||||||
|
{
|
||||||
|
BidLeadStats.Clear();
|
||||||
|
report.Steps.Add("misure dell'anticipo azzerate");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.Exports)
|
||||||
|
{
|
||||||
|
var (n, bytes) = ClearExports();
|
||||||
|
report.FilesDeleted += n;
|
||||||
|
report.BytesFreed += bytes;
|
||||||
|
report.Steps.Add($"esportazioni: {n} file tolti");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.LegacyArchives)
|
||||||
|
{
|
||||||
|
var (n, bytes) = DeleteAll(report, "archivi mensili", LegacyFiles());
|
||||||
|
report.FilesDeleted += n;
|
||||||
|
report.BytesFreed += bytes;
|
||||||
|
report.Steps.Add($"archivi mensili: {n} file tolti");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.Learning)
|
||||||
|
{
|
||||||
|
Try(report, "apprendimento", () =>
|
||||||
|
{
|
||||||
|
Ml.LearningService.Forget();
|
||||||
|
Ml.LatencyModel.Forget();
|
||||||
|
});
|
||||||
|
var (n, bytes) = DeleteAll(report, "apprendimento", ListFiles(LearningFolder, "*"));
|
||||||
|
report.FilesDeleted += n;
|
||||||
|
report.BytesFreed += bytes;
|
||||||
|
report.Steps.Add("apprendimento azzerato: ristudia i dossier al prossimo avvio");
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Svuota la sola cartella delle esportazioni. Restituisce file tolti e byte liberati.</summary>
|
||||||
|
public static (int Files, long Bytes) ClearExports()
|
||||||
|
{
|
||||||
|
var report = new Report();
|
||||||
|
return DeleteAll(report, "esportazioni", ListFiles(AppPaths.ExportFolder, "*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Attrezzi ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Gli archivi mensili delle versioni precedenti: aste-AAAA-MM.jsonl e .json.</summary>
|
||||||
|
private static List<string> LegacyFiles()
|
||||||
|
{
|
||||||
|
var folder = AppPaths.StatsFolder;
|
||||||
|
if (!Directory.Exists(folder)) return new List<string>();
|
||||||
|
|
||||||
|
return Directory.EnumerateFiles(folder, "aste-*.json*")
|
||||||
|
.Where(f =>
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(f);
|
||||||
|
return name.Length >= 12 && char.IsDigit(name[5]) &&
|
||||||
|
(name.EndsWith(".jsonl", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
name.EndsWith(".json", StringComparison.OrdinalIgnoreCase));
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ListFiles(string folder, string pattern)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.Exists(folder)
|
||||||
|
? Directory.EnumerateFiles(folder, pattern).ToList()
|
||||||
|
: new List<string>();
|
||||||
|
}
|
||||||
|
catch { return new List<string>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long SizeOf(string path)
|
||||||
|
{
|
||||||
|
try { return File.Exists(path) ? new FileInfo(path).Length : 0; }
|
||||||
|
catch { return 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int Files, long Bytes) DeleteAll(Report report, string label, IEnumerable<string> files)
|
||||||
|
{
|
||||||
|
var n = 0;
|
||||||
|
long bytes = 0;
|
||||||
|
|
||||||
|
foreach (var f in files)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var size = SizeOf(f);
|
||||||
|
File.Delete(f);
|
||||||
|
n++;
|
||||||
|
bytes += size;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
report.Errors.Add($"{label}: {Path.GetFileName(f)} non cancellato ({ex.Message})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (n, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Try(Report report, string label, Action action)
|
||||||
|
{
|
||||||
|
try { action(); }
|
||||||
|
catch (Exception ex) { report.Errors.Add($"{label}: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,6 +87,7 @@ namespace AutoBidder.ViewModels
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
_auctionInfo.BidBeforeDeadlineMs = value;
|
_auctionInfo.BidBeforeDeadlineMs = value;
|
||||||
|
_auctionInfo.BidLeadIsManual = true;
|
||||||
OnPropertyChanged(nameof(BidBeforeDeadlineMs));
|
OnPropertyChanged(nameof(BidBeforeDeadlineMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user