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>
205 lines
8.4 KiB
C#
205 lines
8.4 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using AutoBidder.Utilities;
|
|
|
|
namespace AutoBidder.Controls
|
|
{
|
|
/// <summary>
|
|
/// Scheda Esporta: sceglie quali aste finiscono in un unico file e lo scrive.
|
|
///
|
|
/// <para>Come gli altri pannelli non conosce gli archivi: costruisce un
|
|
/// <see cref="AuctionExportFilter"/> e lascia il lavoro a
|
|
/// <see cref="AuctionExporter"/>. Qui dentro c'è solo la traduzione fra i campi
|
|
/// dell'interfaccia e i criteri — e il conteggio in tempo reale, che serve a non
|
|
/// scoprire dopo un minuto di scrittura che il filtro non selezionava nulla.</para>
|
|
/// </summary>
|
|
public partial class ExportControl : UserControl
|
|
{
|
|
private string? _lastExportPath;
|
|
|
|
public ExportControl()
|
|
{
|
|
InitializeComponent();
|
|
Loaded += (_, _) => RefreshPreview();
|
|
}
|
|
|
|
/// <summary>I criteri come sono impostati adesso.</summary>
|
|
public AuctionExportFilter BuildFilter() => new()
|
|
{
|
|
From = FromDatePicker.SelectedDate,
|
|
To = ToDatePicker.SelectedDate,
|
|
NameContains = string.IsNullOrWhiteSpace(NameFilterTextBox.Text) ? null : NameFilterTextBox.Text.Trim(),
|
|
|
|
IncludeWon = IncludeWonCheckBox.IsChecked == true,
|
|
IncludeLost = IncludeLostCheckBox.IsChecked == true,
|
|
IncludeClosed = IncludeClosedCheckBox.IsChecked == true,
|
|
|
|
OnlyComplete = OnlyCompleteCheckBox.IsChecked == true,
|
|
OnlyWithMyBids = OnlyWithMyBidsCheckBox.IsChecked == true,
|
|
OnlyWithDossier = OnlyWithDossierCheckBox.IsChecked == true,
|
|
|
|
MinFinalPrice = ParseNumber(MinPriceTextBox.Text),
|
|
MaxFinalPrice = ParseNumber(MaxPriceTextBox.Text),
|
|
MaxAuctions = (int)(ParseNumber(MaxAuctionsTextBox.Text) ?? 0),
|
|
|
|
IncludeEvents = IncludeEventsCheckBox.IsChecked == true
|
|
};
|
|
|
|
/// <summary>
|
|
/// Ricalcola quante aste corrispondono. Si fa a ogni modifica dei filtri: sapere
|
|
/// prima che il risultato è vuoto — o che sono duemila — evita di scoprirlo a file
|
|
/// scritto.
|
|
/// </summary>
|
|
public void RefreshPreview()
|
|
{
|
|
try
|
|
{
|
|
var selected = AuctionExporter.Select(BuildFilter());
|
|
|
|
MatchCountText.Text = selected.Count == 1 ? "1 asta" : $"{selected.Count} aste";
|
|
|
|
var complete = selected.Count(r => r.IsComplete);
|
|
CompleteCountText.Text = $"{complete} complete";
|
|
|
|
ExportButton.IsEnabled = selected.Count > 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MatchCountText.Text = "—";
|
|
CompleteCountText.Text = ex.Message;
|
|
}
|
|
}
|
|
|
|
/// <summary>Mostra l'esito dell'esportazione appena fatta.</summary>
|
|
public void ShowResult(AuctionExportResult result)
|
|
{
|
|
if (result.Success)
|
|
{
|
|
_lastExportPath = result.Path;
|
|
|
|
var size = result.Bytes >= 1024 * 1024
|
|
? $"{result.Bytes / 1024.0 / 1024.0:F1} MB"
|
|
: $"{Math.Max(1, result.Bytes / 1024)} kB";
|
|
|
|
ResultText.Text = $"{result.AuctionCount} aste ({result.WithEvents} con eventi), {size} — {result.Path}";
|
|
ResultText.SetResourceReference(ForegroundProperty, "Brush.Text");
|
|
OpenLastFileButton.Visibility = Visibility.Visible;
|
|
}
|
|
else
|
|
{
|
|
ResultText.Text = result.Message;
|
|
ResultText.SetResourceReference(ForegroundProperty, "Brush.Warning");
|
|
OpenLastFileButton.Visibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
|
|
/// <summary>Percorso dell'ultimo file scritto, per il pulsante "Apri il file".</summary>
|
|
public string? LastExportPath => _lastExportPath;
|
|
|
|
// ── Eventi dell'interfaccia ──────────────────────────────────────
|
|
|
|
private void PreviewButton_Click(object sender, RoutedEventArgs e) => RefreshPreview();
|
|
|
|
private void Filter_Changed(object sender, RoutedEventArgs e) => RefreshPreview();
|
|
|
|
private void Last7Days_Click(object sender, RoutedEventArgs e) => SetPeriod(7);
|
|
|
|
private void Last30Days_Click(object sender, RoutedEventArgs e) => SetPeriod(30);
|
|
|
|
private void SetPeriod(int days)
|
|
{
|
|
FromDatePicker.SelectedDate = DateTime.Today.AddDays(-days);
|
|
ToDatePicker.SelectedDate = DateTime.Today;
|
|
RefreshPreview();
|
|
}
|
|
|
|
private void ResetFiltersButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
FromDatePicker.SelectedDate = null;
|
|
ToDatePicker.SelectedDate = null;
|
|
NameFilterTextBox.Text = "";
|
|
MinPriceTextBox.Text = "";
|
|
MaxPriceTextBox.Text = "";
|
|
MaxAuctionsTextBox.Text = "0";
|
|
|
|
IncludeWonCheckBox.IsChecked = true;
|
|
IncludeLostCheckBox.IsChecked = true;
|
|
IncludeClosedCheckBox.IsChecked = true;
|
|
OnlyCompleteCheckBox.IsChecked = false;
|
|
OnlyWithMyBidsCheckBox.IsChecked = false;
|
|
OnlyWithDossierCheckBox.IsChecked = false;
|
|
IncludeEventsCheckBox.IsChecked = true;
|
|
|
|
RefreshPreview();
|
|
}
|
|
|
|
private void ExportButton_Click(object sender, RoutedEventArgs e)
|
|
=> RaiseEvent(new RoutedEventArgs(ExportRequestedEvent, this));
|
|
|
|
private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
|
|
=> 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)
|
|
=> RaiseEvent(new RoutedEventArgs(OpenLastFileClickedEvent, this));
|
|
|
|
/// <summary>
|
|
/// Legge un numero accettando sia la virgola sia il punto: chi scrive "12,50" e chi
|
|
/// scrive "12.50" intende la stessa cosa, e rifiutarne uno dei due sarebbe pedanteria.
|
|
/// </summary>
|
|
private static double? ParseNumber(string? text)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text)) return null;
|
|
|
|
var clean = text.Trim().Replace(',', '.');
|
|
|
|
return double.TryParse(clean, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)
|
|
? value
|
|
: null;
|
|
}
|
|
|
|
// ── Routed events ────────────────────────────────────────────────
|
|
|
|
public static readonly RoutedEvent ExportRequestedEvent = EventManager.RegisterRoutedEvent(
|
|
"ExportRequested", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
|
|
|
public static readonly RoutedEvent OpenExportFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
|
"OpenExportFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
|
|
|
public static readonly RoutedEvent OpenLastFileClickedEvent = EventManager.RegisterRoutedEvent(
|
|
"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
|
|
{
|
|
add { AddHandler(ExportRequestedEvent, value); }
|
|
remove { RemoveHandler(ExportRequestedEvent, value); }
|
|
}
|
|
|
|
public event RoutedEventHandler OpenExportFolderClicked
|
|
{
|
|
add { AddHandler(OpenExportFolderClickedEvent, value); }
|
|
remove { RemoveHandler(OpenExportFolderClickedEvent, value); }
|
|
}
|
|
|
|
public event RoutedEventHandler OpenLastFileClicked
|
|
{
|
|
add { AddHandler(OpenLastFileClickedEvent, value); }
|
|
remove { RemoveHandler(OpenLastFileClickedEvent, value); }
|
|
}
|
|
}
|
|
}
|