diff --git a/Mimante/Controls/LearningControl.xaml b/Mimante/Controls/LearningControl.xaml
new file mode 100644
index 0000000..c989917
--- /dev/null
+++ b/Mimante/Controls/LearningControl.xaml
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Mimante/Controls/LearningControl.xaml.cs b/Mimante/Controls/LearningControl.xaml.cs
new file mode 100644
index 0000000..9b93773
--- /dev/null
+++ b/Mimante/Controls/LearningControl.xaml.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Threading;
+using AutoBidder.Ml;
+using AutoBidder.Utilities;
+
+namespace AutoBidder.Controls
+{
+ ///
+ /// La scheda dell'apprendimento: cosa sa il modello, cosa ha deciso di recente, cosa
+ /// aspettarsi per prodotto e ora, e l'ultima valutazione.
+ ///
+ /// Legge direttamente da : non ha bisogno della
+ /// finestra principale per niente, e si aggiorna da sola ogni pochi secondi mentre è
+ /// visibile. Tutto il resto dell'applicazione non sa che esiste.
+ ///
+ public partial class LearningControl : UserControl
+ {
+ private readonly DispatcherTimer _refresh;
+ private CancellationTokenSource? _evaluation;
+
+ private sealed class PesoRow
+ {
+ public string Nome { get; init; } = "";
+ public double Peso { get; init; }
+ public string PesoDisplay => Peso.ToString("+0.000;-0.000");
+ public string Effetto => Math.Abs(Peso) < 0.05 ? "quasi nullo"
+ : Peso > 0 ? "più senza risposta" : "più risposte";
+ }
+
+ private sealed class ProfiloRow
+ {
+ public string Prodotto { get; init; } = "";
+ public string Fascia { get; init; } = "";
+ public string Giorno { get; init; } = "";
+ public long Aste { get; init; }
+ public string Puntate { get; init; } = "";
+ public string Chiusura { get; init; } = "";
+ }
+
+ private sealed class DecisioneRow
+ {
+ public string Ora { get; init; } = "";
+ public string Asta { get; init; } = "";
+ public string Prezzo { get; init; } = "";
+ public string Prob { get; init; } = "";
+ public string Ev { get; init; } = "";
+ public string Esito { get; init; } = "";
+ }
+
+ public LearningControl()
+ {
+ InitializeComponent();
+
+ _refresh = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
+ _refresh.Tick += (_, _) => { if (IsVisible) Refresh(); };
+
+ IsVisibleChanged += (_, e) =>
+ {
+ if ((bool)e.NewValue) { Refresh(); _refresh.Start(); }
+ else _refresh.Stop();
+ };
+ }
+
+ /// Rilegge tutto dal servizio. Costa poco: sono numeri già in memoria.
+ public void Refresh()
+ {
+ try
+ {
+ var settings = SettingsManager.Load();
+ var apprese = LearningService.AuctionsLearned;
+ var soglia = Math.Max(1, settings.LearningMinAuctions);
+ var pronto = LearningService.IsReady(settings);
+ var calib = LearningService.CalibrationFactor;
+
+ PillAuctions.Text = $"{apprese:N0} aste";
+ PillUpdates.Text = $"{LearningService.ModelUpdates:N0} esempi";
+ PillCalibration.Text = $"calibrazione {calib:N2}";
+ PillReady.Text = pronto ? "pronto" : $"in ascolto ({apprese}/{soglia})";
+ PillReady.Foreground = (System.Windows.Media.Brush)FindResource(pronto ? "Brush.Success" : "Brush.Warning");
+
+ StatAuctions.Text = apprese.ToString("N0");
+ StatUpdates.Text = LearningService.ModelUpdates.ToString("N0");
+ StatProfile.Text = LearningService.ProfileAuctions.ToString("N0");
+ StatCalibration.Text = calib.ToString("N2");
+ StatThreshold.Text = soglia.ToString("N0");
+ StatBootstrap.Text = LearningService.BootstrapRunning ? "in corso" : "fermo";
+
+ var decisioni = LearningService.RecentDecisions();
+ var fermate = decisioni.Count(d => d.Blocked);
+ StatDecisions.Text = decisioni.Count == 0
+ ? "Nessuna decisione ancora: il modello risponde quando il motore è pronto a puntare."
+ : $"Ultime {decisioni.Count} decisioni: {fermate} fermate, {decisioni.Count - fermate} lasciate passare" +
+ (settings.LearningGateEnabled ? "." : ". Il cancello è spento nelle impostazioni: il modello parla ma non decide.");
+
+ WeightsGrid.ItemsSource = LearningService.Pesi()
+ .Take(30)
+ .Select(p => new PesoRow { Nome = p.Nome, Peso = p.Peso })
+ .ToList();
+
+ DecisionsGrid.ItemsSource = decisioni
+ .Select(d => new DecisioneRow
+ {
+ Ora = d.At.ToString("HH:mm:ss"),
+ Asta = d.Auction,
+ Prezzo = d.Price.ToString("F2") + " €",
+ Prob = d.Probability.ToString("P2"),
+ Ev = d.ExpectedValue.ToString("+0.000;-0.000") + " €",
+ Esito = !d.Ready ? "parere" : d.Blocked ? "fermata" : "passa"
+ })
+ .ToList();
+
+ ProfileGrid.ItemsSource = LearningService.ProfileRows(150)
+ .Select(r => new ProfiloRow
+ {
+ Prodotto = r.Product,
+ Fascia = FasciaLabel(r.HourBand),
+ Giorno = r.Weekend ? "festivo" : "feriale",
+ Aste = r.Auctions,
+ Puntate = double.IsNaN(r.WinnerBids) ? "—" : r.WinnerBids.ToString("N0"),
+ Chiusura = double.IsNaN(r.CloseRatio) ? "—" : r.CloseRatio.ToString("P1")
+ })
+ .ToList();
+
+ if (_evaluation == null)
+ {
+ var ultima = LearningService.LastEvaluation();
+ if (!string.IsNullOrWhiteSpace(ultima)) EvaluationBox.Text = ultima;
+ }
+ }
+ catch (Exception ex)
+ {
+ StatDecisions.Text = $"Aggiornamento non riuscito: {ex.Message}";
+ }
+ }
+
+ private static string FasciaLabel(int band) => band switch
+ {
+ 0 => "0-8", 1 => "9", 2 => "10-12", 3 => "13-17", 4 => "18-20", _ => "21-23"
+ };
+
+ private void RefreshButton_Click(object sender, RoutedEventArgs e) => Refresh();
+
+ private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var folder = Path.Combine(AppPaths.StatsFolder, "Apprendimento");
+ Directory.CreateDirectory(folder);
+ Process.Start(new ProcessStartInfo { FileName = folder, UseShellExecute = true });
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Cartella non apribile: {ex.Message}", "Apprendimento",
+ MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ }
+
+ private async void EvaluateButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_evaluation != null)
+ {
+ _evaluation.Cancel();
+ return;
+ }
+
+ _evaluation = new CancellationTokenSource();
+ EvaluateButton.Content = "Annulla";
+ EvaluationBox.Text = "Valutazione in corso: lettura dei dossier…";
+
+ try
+ {
+ // Al massimo tremila dossier, i più recenti: bastano a giudicare, e la
+ // lettura di tutto l'archivio in sottofondo mentre le aste corrono non serve.
+ var report = await LearningService.EvaluateAsync(3000,
+ msg => Dispatcher.BeginInvoke(() => EvaluationBox.Text = "Valutazione in corso: " + msg),
+ _evaluation.Token);
+
+ EvaluationBox.Text = report.Text;
+ }
+ catch (OperationCanceledException)
+ {
+ EvaluationBox.Text = "Valutazione annullata.";
+ }
+ catch (Exception ex)
+ {
+ EvaluationBox.Text = $"Valutazione non riuscita: {ex.Message}";
+ }
+ finally
+ {
+ _evaluation = null;
+ EvaluateButton.Content = "Valuta ora";
+ }
+ }
+
+ private async void RetrainButton_Click(object sender, RoutedEventArgs e)
+ {
+ var answer = MessageBox.Show(
+ "Butto modello, profilo ed elenco dei dossier letti, e ricomincio a studiare tutto l'archivio.\n\n" +
+ "I dossier e lo storico non vengono toccati. Ci vorranno un paio di minuti in sottofondo.\n\nProcedo?",
+ "Ricomincia da capo", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+
+ if (answer != MessageBoxResult.Yes) return;
+
+ RetrainButton.IsEnabled = false;
+ try
+ {
+ await LearningService.RetrainFromScratchAsync(SettingsManager.Load());
+ }
+ finally
+ {
+ RetrainButton.IsEnabled = true;
+ Refresh();
+ }
+ }
+ }
+}
diff --git a/Mimante/Core/MainWindow.ControlEvents.cs b/Mimante/Core/MainWindow.ControlEvents.cs
index 39ad737..e67466e 100644
--- a/Mimante/Core/MainWindow.ControlEvents.cs
+++ b/Mimante/Core/MainWindow.ControlEvents.cs
@@ -54,6 +54,12 @@ namespace AutoBidder
LoadStatistics();
}
+ private void TabApprendimento_Checked(object sender, RoutedEventArgs e)
+ {
+ ShowPanel(Learning);
+ Learning.Refresh();
+ }
+
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
{
try
@@ -74,7 +80,8 @@ namespace AutoBidder
{
// Prevent NullReferenceException during initialization
if (AuctionMonitor == null || Browser == null || StatisticsPanel == null ||
- Settings == null || FreeBids == null || Products == null || Export == null)
+ Settings == null || FreeBids == null || Products == null || Export == null ||
+ Learning == null)
return;
// Hide all panels
@@ -85,6 +92,7 @@ namespace AutoBidder
StatisticsPanel.Visibility = Visibility.Collapsed;
Export.Visibility = Visibility.Collapsed;
Settings.Visibility = Visibility.Collapsed;
+ Learning.Visibility = Visibility.Collapsed;
// Show selected panel
if (panelToShow != null)
diff --git a/Mimante/MainWindow.xaml b/Mimante/MainWindow.xaml
index 57b8ca7..85c9b44 100644
--- a/Mimante/MainWindow.xaml
+++ b/Mimante/MainWindow.xaml
@@ -75,6 +75,12 @@
Style="{StaticResource VerticalTabButton}"
Checked="TabEsporta_Checked"/>
+
+
+
+
+
diff --git a/Mimante/Ml/AuctionProfileStats.cs b/Mimante/Ml/AuctionProfileStats.cs
index 213e14e..56b39f7 100644
--- a/Mimante/Ml/AuctionProfileStats.cs
+++ b/Mimante/Ml/AuctionProfileStats.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Text.Json;
namespace AutoBidder.Ml
@@ -117,6 +118,42 @@ namespace AutoBidder.Ml
return new[] { "g", "p:" + prod, "p:" + prod + "|h" + band + "|" + day };
}
+ /// Una riga del profilo, leggibile: per mostrarlo.
+ public readonly record struct Row(string Product, int HourBand, bool Weekend, long Auctions, double WinnerBids, double CloseRatio);
+
+ ///
+ /// Le combinazioni esatte con almeno un'asta, dalla più numerosa. Solo le esatte:
+ /// le righe di prodotto e quella globale sono medie delle esatte, e mostrarle
+ /// insieme confonderebbe.
+ ///
+ public IReadOnlyList Rows(int max = 200)
+ {
+ var list = new List();
+ lock (_sync)
+ {
+ foreach (var kv in _cells)
+ {
+ // chiave esatta: "p:{prodotto}|h{fascia}|{fer|fest}"
+ var k = kv.Key;
+ if (!k.StartsWith("p:", StringComparison.Ordinal)) continue;
+ var h = k.LastIndexOf("|h", StringComparison.Ordinal);
+ if (h < 0) continue;
+
+ var product = k.Substring(2, h - 2);
+ var rest = k[(h + 2)..]; // "{fascia}|{fer|fest}"
+ var bar = rest.IndexOf('|');
+ if (bar < 0 || !int.TryParse(rest[..bar], out var band)) continue;
+ var weekend = rest[(bar + 1)..] == "fest";
+
+ var c = kv.Value;
+ list.Add(new Row(product, band, weekend, c.N,
+ c.NBids > 0 ? c.MeanBids : double.NaN,
+ c.NRatio > 0 ? c.MeanRatio : double.NaN));
+ }
+ }
+ return list.OrderByDescending(r => r.Auctions).Take(max).ToList();
+ }
+
// ── Persistenza ──────────────────────────────────────────────────
private sealed class Snapshot
diff --git a/Mimante/Ml/LearningEvaluator.cs b/Mimante/Ml/LearningEvaluator.cs
new file mode 100644
index 0000000..06eb017
--- /dev/null
+++ b/Mimante/Ml/LearningEvaluator.cs
@@ -0,0 +1,217 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+namespace AutoBidder.Ml
+{
+ ///
+ /// Valuta il modello sui dossier veri, con una divisione onesta nel tempo: si impara
+ /// dai più vecchi e si giudica sui più recenti, mai visti. Poi, sui recenti, la
+ /// valutazione prequenziale: ogni asta prima prevista e poi appresa, in ordine di
+ /// tempo, che è esattamente la sequenza del motore in produzione.
+ ///
+ /// Sta in una classe a sé perché la usano in due: il test da riga di comando
+ /// (target Apprendimento) e la scheda nell'applicazione. Una sola
+ /// implementazione, così i numeri che vede l'utente sono gli stessi che vede chi
+ /// sviluppa.
+ ///
+ /// Cosa misura. Quanto il modello separa le puntate rimaste senza risposta
+ /// (sollevamento nella cima della classifica), se la probabilità è nella scala giusta
+ /// (calibrazione per fasce), e sulle nostre puntate vere cosa avrebbe fatto il cancello
+ /// del valore atteso: quante fermate, e quante di quelle erano invece vincenti.
+ ///
+ public static class LearningEvaluator
+ {
+ public sealed class Report
+ {
+ public string Text { get; init; } = "";
+ public int Files { get; init; }
+ public int Usable { get; init; }
+ public double Lift5 { get; init; }
+ public double CalibrationFactor { get; init; }
+ public int MyBids { get; init; }
+ public int MyBlocked { get; init; }
+ public int MyBlockedWinning { get; init; }
+ public bool Enough => Usable >= 20;
+ }
+
+ private sealed record Row(double P, bool Unanswered, bool Mine, double Ev);
+
+ public static Report Run(string folder, int maxFiles, string? me, Action? progress = null, CancellationToken ct = default)
+ {
+ var files = Directory.GetFiles(folder, "*.jsonl").OrderBy(Path.GetFileName, StringComparer.Ordinal).ToList();
+ if (maxFiles > 0 && files.Count > maxFiles) files = files.Skip(files.Count - maxFiles).ToList();
+
+ var sessions = new List();
+ var letti = 0;
+ foreach (var f in files)
+ {
+ ct.ThrowIfCancellationRequested();
+ var s = DossierTrainingReader.ReadFile(f, me);
+ if (s != null && s.IsUsable) sessions.Add(s);
+ if (++letti % 200 == 0) progress?.Invoke($"letti {letti:N0} dossier su {files.Count:N0}…");
+ }
+
+ var report = new StringBuilder();
+ report.AppendLine($"Dossier letti: {files.Count:N0}, utilizzabili: {sessions.Count:N0}");
+
+ if (sessions.Count < 20)
+ {
+ report.AppendLine("Troppo pochi dossier utilizzabili per una valutazione.");
+ return new Report { Text = report.ToString(), Files = files.Count, Usable = sessions.Count };
+ }
+
+ progress?.Invoke("addestramento e confronto delle configurazioni…");
+
+ var split = (int)(sessions.Count * 0.7);
+ var train = sessions.Take(split).ToList();
+ var test = sessions.Skip(split).ToList();
+
+ // Le variabili si costruiscono una volta sola: leggere i file è la parte lenta.
+ var trainX = train.SelectMany(s => DossierTrainingReader.Examples(s))
+ .Select(e => (X: BidFeatures.Build(e.Context), Y: e.Unanswered)).ToList();
+
+ var perAsta = test.Select(s => DossierTrainingReader.Examples(s)
+ .Select(e => (
+ X: BidFeatures.Build(e.Context), Y: e.Unanswered, Mine: e.Mine,
+ Margine: s.BuyNowPrice is > 0 ? s.BuyNowPrice.Value - e.Context.AuctionDepthCents / 100.0 - 0.01 : double.NaN))
+ .ToList()).ToList();
+ var testX = perAsta.SelectMany(a => a).ToList();
+
+ var n = testX.Count;
+ var positives = testX.Count(r => r.Y);
+ var baseRate = (double)positives / Math.Max(1, n);
+
+ report.AppendLine($"Addestramento: {train.Count:N0} aste, {trainX.Count:N0} puntate");
+ report.AppendLine($"Valutazione : {test.Count:N0} aste, {n:N0} puntate, {positives:N0} senza risposta ({baseRate:P3})");
+ report.AppendLine();
+
+ // ── Confronto fra configurazioni (modello congelato) ─────────────
+ //
+ // Serve a scegliere; non è la misura finale. Un modello congelato sui vecchi e
+ // applicato ai nuovi sconta la deriva del mercato, che nell'uso online non
+ // subisce: la misura che conta è la prequenziale qui sotto.
+ const double costo = 0.20;
+ var configs = new List<(string Nome, double Lr, double L2)>();
+ foreach (var lr in new[] { 0.01, 0.003 })
+ foreach (var l2 in new[] { 1e-4, 0.0 })
+ configs.Add(($"passo {lr:0.000}, L2 {l2:0.######}", lr, l2));
+
+ report.AppendLine("Configurazione (modello congelato) | sollev. 5% | prevista/osservata (cima 10%) | prevista/osservata (tutte)");
+ report.AppendLine("-------------------------------------+------------+-------------------------------+---------------------------");
+
+ OnlineLogit? best = null; var bestName = ""; var bestScore = double.MaxValue;
+ foreach (var (nome, lr, l2) in configs)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var modello = new OnlineLogit(BidFeatures.Size) { LearningRate = lr, L2 = l2 };
+ foreach (var (x, y) in trainX) modello.Update(x, y);
+
+ var ps = testX.Select(r => modello.Predict(r.X)).ToArray();
+ var order = Enumerable.Range(0, n).OrderByDescending(i => ps[i]).ToArray();
+ var k5 = Math.Max(1, n / 20); var k10 = Math.Max(1, n / 10);
+ var lift5 = order.Take(k5).Count(i => testX[i].Y) / (double)k5 / Math.Max(1e-9, baseRate);
+ var top10 = order.Take(k10).ToArray();
+ var ratioTop = top10.Average(i => ps[i]) / Math.Max(1e-9, top10.Average(i => testX[i].Y ? 1.0 : 0.0));
+ var ratioAll = ps.Average() / Math.Max(1e-9, baseRate);
+
+ report.AppendLine($"{nome,-37} | {lift5,9:N2}x | {ratioTop,29:N2} | {ratioAll,25:N2}");
+
+ var score = Math.Abs(Math.Log(Math.Max(1e-9, ratioTop))) + Math.Abs(Math.Log(Math.Max(1e-9, ratioAll)));
+ if (lift5 > 2.0 && score < bestScore) { bestScore = score; best = modello; bestName = nome; }
+ }
+ report.AppendLine();
+
+ if (best == null)
+ {
+ report.AppendLine("Nessuna configurazione separa abbastanza (sollevamento al 5% > 2x).");
+ return new Report { Text = report.ToString(), Files = files.Count, Usable = sessions.Count };
+ }
+
+ report.AppendLine($"Scelta: {bestName}");
+ report.AppendLine();
+
+ // ── Prequenziale: come nell'uso vero ─────────────────────────────
+ progress?.Invoke("valutazione prequenziale…");
+ var model = best;
+ var rows = new List();
+ foreach (var esempi in perAsta)
+ {
+ ct.ThrowIfCancellationRequested();
+ foreach (var r in esempi)
+ {
+ var p = model.PredictCalibrated(r.X);
+ rows.Add(new Row(p, r.Y, r.Mine, double.IsNaN(r.Margine) ? double.NaN : p * r.Margine - costo));
+ }
+ foreach (var r in esempi) model.Update(r.X, r.Y);
+ }
+
+ report.AppendLine($"Valutazione prequenziale (ogni asta prima prevista, poi appresa), fattore di calibrazione finale {model.CalibrationFactor:N2}:");
+
+ var meanPos = rows.Where(r => r.Unanswered).Select(r => r.P).DefaultIfEmpty(0).Average();
+ var meanNeg = rows.Where(r => !r.Unanswered).Select(r => r.P).DefaultIfEmpty(0).Average();
+ report.AppendLine($"P media sulle puntate senza risposta : {meanPos:P3}");
+ report.AppendLine($"P media sulle altre : {meanNeg:P3}");
+ report.AppendLine($"rapporto : {meanPos / Math.Max(meanNeg, 1e-9):N2}x");
+ report.AppendLine();
+
+ var sorted = rows.OrderByDescending(r => r.P).ToList();
+ report.AppendLine("Sollevamento nella cima della classifica (precisione / tasso di base):");
+ double liftTop5 = 0;
+ foreach (var frac in new[] { 0.01, 0.05, 0.10, 0.25 })
+ {
+ var k = Math.Max(1, (int)(n * frac));
+ var prec = sorted.Take(k).Count(r => r.Unanswered) / (double)k;
+ var lift = prec / Math.Max(1e-9, baseRate);
+ if (frac == 0.05) liftTop5 = lift;
+ report.AppendLine($" cima {frac:P0}: {k,8:N0} puntate, precisione {prec:P2}, sollevamento {lift:N2}x");
+ }
+ report.AppendLine();
+
+ report.AppendLine("Calibrazione per fasce di probabilità prevista:");
+ var edges = new[] { 0.0, 0.001, 0.0025, 0.005, 0.01, 0.02, 0.05, 1.0 };
+ for (var i = 0; i < edges.Length - 1; i++)
+ {
+ var bin = rows.Where(r => r.P >= edges[i] && r.P < edges[i + 1]).ToList();
+ if (bin.Count == 0) continue;
+ report.AppendLine($" P in [{edges[i]:P2}, {edges[i + 1]:P2}): {bin.Count,8:N0} puntate, prevista {bin.Average(r => r.P):P3}, osservata {bin.Average(r => r.Unanswered ? 1.0 : 0.0):P3}");
+ }
+ report.AppendLine();
+
+ var mie = rows.Where(r => r.Mine && !double.IsNaN(r.Ev)).ToList();
+ var fermate = mie.Where(r => r.Ev < 0).ToList();
+ if (mie.Count > 0)
+ {
+ var passate = mie.Where(r => r.Ev >= 0).ToList();
+ report.AppendLine($"Le mie puntate nel periodo di valutazione: {mie.Count:N0}, di cui finali {mie.Count(r => r.Unanswered):N0}");
+ report.AppendLine($" il cancello ne avrebbe lasciate passare {passate.Count:N0} (finali: {passate.Count(r => r.Unanswered):N0})");
+ report.AppendLine($" e ne avrebbe fermate {fermate.Count:N0} (di cui finali, cioè vincenti perse: {fermate.Count(r => r.Unanswered):N0})");
+ }
+ else
+ {
+ report.AppendLine("Nessuna mia puntata nel periodo di valutazione (serve il nome utente, o non ci sono).");
+ }
+ report.AppendLine();
+
+ report.AppendLine("Pesi più forti (cosa ha imparato):");
+ foreach (var (nome, peso) in BidFeatures.Names.Select((nm, i) => (nm, model.Weights[i])).OrderByDescending(t => Math.Abs(t.Item2)).Take(15))
+ report.AppendLine($" {peso,+8:F3} {nome}");
+
+ return new Report
+ {
+ Text = report.ToString(),
+ Files = files.Count,
+ Usable = sessions.Count,
+ Lift5 = liftTop5,
+ CalibrationFactor = model.CalibrationFactor,
+ MyBids = mie.Count,
+ MyBlocked = fermate.Count,
+ MyBlockedWinning = fermate.Count(r => r.Unanswered)
+ };
+ }
+ }
+}
diff --git a/Mimante/Ml/LearningService.cs b/Mimante/Ml/LearningService.cs
index c7d6e53..84ca909 100644
--- a/Mimante/Ml/LearningService.cs
+++ b/Mimante/Ml/LearningService.cs
@@ -90,6 +90,123 @@ namespace AutoBidder.Ml
}
}
+ /// Il profilo per prodotto e fascia, dalla combinazione più numerosa.
+ public static IReadOnlyList ProfileRows(int max = 200)
+ {
+ lock (Sync) return _profile.Rows(max);
+ }
+
+ // ── Ultime decisioni, per la scheda ──────────────────────────────
+ //
+ // Il motore chiede al modello a ogni giro utile, e la risposta finisce nel
+ // registro dell'asta. Qui se ne tiene una coda corta per vederle tutte insieme:
+ // e' l'unico modo di capire a colpo d'occhio se il cancello sta fermando troppo o
+ // troppo poco.
+
+ /// Una risposta data al motore.
+ public readonly record struct Decision(
+ DateTime At, string Auction, double Price, double Probability, double ExpectedValue, bool Blocked, bool Ready);
+
+ private const int MaxDecisions = 300;
+ private static readonly Queue _decisions = new();
+
+ public static void RecordDecision(string auction, double price, double probability, double expectedValue, bool blocked, bool ready)
+ {
+ lock (_decisions)
+ {
+ // Lo stesso giro dell'asta produce molte risposte uguali di fila: si tiene
+ // solo il cambiamento, altrimenti la coda si riempie di una sola asta.
+ if (_decisions.Count > 0)
+ {
+ var last = _decisions.Last();
+ if (last.Auction == auction && last.Blocked == blocked &&
+ Math.Abs(last.Price - price) < 0.005 && (DateTime.Now - last.At).TotalSeconds < 2)
+ return;
+ }
+
+ _decisions.Enqueue(new Decision(DateTime.Now, auction, price, probability, expectedValue, blocked, ready));
+ while (_decisions.Count > MaxDecisions) _decisions.Dequeue();
+ }
+ }
+
+ public static IReadOnlyList RecentDecisions()
+ {
+ lock (_decisions) return _decisions.Reverse().ToList();
+ }
+
+ // ── Valutazione dall'applicazione ────────────────────────────────
+
+ private static string EvaluationFile => Path.Combine(Folder, "valutazione.txt");
+
+ /// L'ultimo rapporto di valutazione salvato, se c'è.
+ public static string? LastEvaluation()
+ {
+ try { return File.Exists(EvaluationFile) ? File.ReadAllText(EvaluationFile) : null; }
+ catch { return null; }
+ }
+
+ ///
+ /// Valuta il modello sui dossier di questa macchina, in sottofondo, e salva il
+ /// rapporto. È la stessa valutazione del target da riga di comando.
+ ///
+ public static Task EvaluateAsync(int maxFiles, Action? progress, System.Threading.CancellationToken ct)
+ {
+ return Task.Run(() =>
+ {
+ var me = AuctionDossier.CurrentUsername;
+ var report = LearningEvaluator.Run(AppPaths.AuctionLogFolder, maxFiles, string.IsNullOrEmpty(me) ? null : me, progress, ct);
+
+ try
+ {
+ Directory.CreateDirectory(Folder);
+ lock (SaveSync) File.WriteAllText(EvaluationFile, $"Valutazione del {DateTime.Now:dd/MM/yyyy HH:mm}\n\n" + report.Text);
+ }
+ catch { /* il rapporto e' comunque restituito */ }
+
+ return report;
+ }, ct);
+ }
+
+ // ── Ricominciare da capo ─────────────────────────────────────────
+
+ ///
+ /// Butta modello, profilo ed elenco dei dossier letti, e ricomincia a studiare
+ /// dall'archivio. Serve quando si cambiano le variabili o si sospetta che il
+ /// modello abbia imparato da dati sbagliati. Non tocca i dossier.
+ ///
+ public static Task RetrainFromScratchAsync(AppSettings settings)
+ {
+ lock (Sync)
+ {
+ _model = new OnlineLogit(BidFeatures.Size);
+ _profile = new AuctionProfileStats();
+ _ingested.Clear();
+ _auctionsLearned = 0;
+ }
+
+ lock (_decisions) _decisions.Clear();
+
+ try
+ {
+ lock (SaveSync)
+ {
+ foreach (var f in new[] { ModelFile, ProfileFile, ManifestFile })
+ if (File.Exists(f)) File.Delete(f);
+ }
+ }
+ catch (Exception ex)
+ {
+ 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 ────────────────────────────────────────────────────────
public static void Start(AppSettings settings)
diff --git a/Mimante/Modifiche.txt b/Mimante/Modifiche.txt
index 1b07c86..e69de29 100644
--- a/Mimante/Modifiche.txt
+++ b/Mimante/Modifiche.txt
@@ -1,21 +0,0 @@
-Ottimo! Ora altre modifiche:
-- Aggiungi più opzioni di pulizia e un ricalcolo dei valori statistici dei prodotti basandosi sulle aste concluse dopo la pulizia
-- Metti la cartella "Documenti" come percorso predefinito per le impostazioni e i dati delle aste ma non per l'autenticazione
-
-Inoltre vorrei rendere più fluido il programma. Secondo me allo stato attuale ci sono tanti input/output su disco dati da log e salvataggio delle aste.
-Secondo me, su scelta dell'utente, potrei evitare di caricare alcune parti del programma come per esempio la registrazione dei log su disco, la registrazione di tutte le puntate, ecc ecc per migliorare la velocità del programma.
-Evitando di caricare fisicamente queste parti, riavviando il programma quando si cambiano queste impostazioni il programma dovrebbe snellirsi e velocizzarsi? Tu che dici? Se sei d'accordo con me effettua queste modifiche mettendo in piedi un sistema che produca l'effeto desiderato, oltre che migliorare la velocità del programma.
-
-Infine, secondo te, sarebbe possibile mettere in piedi un sistema di machine learning che capisca da solo, in base al traffico, al numero di puntate, all'articolo, ecc ecc, quando sia il momento giusto per puntare e quindi poi puntare? Ricordo che l'idea è sempre quella di usare meno puntate possibili per vincere le aste. Meno puntate e meglio è. Dimmi anche quali dati ti servono. Non effettuare modifiche in questo senso ma rispondimi solo dopo aver eseguito le richieste precedenti.
-
-Analizza la seguente documentazione e trova un modo per implementarla e dimmi se vale la pena implementarla, integrandola con il resto del software e con l'algoritmo di machine learning:
-Modellizzazione Teoretica e Ottimizzazione Algoritmica per Aste al Centesimo: Un Approccio Integrato di Teoria dei Giochi, Processi Stocastici e Arresto Ottimale1. Architettura Matematica della Spesa, Valore Atteso e Controllo del RischioLe aste al centesimo (penny auctions) rappresentano una classe dinamica di aste a pagamento per offerta (pay-to-bid auctions), in cui ogni rilancio richiede il pagamento di una commissione fissa e non rimborsabile, incrementa il prezzo nominale dell'oggetto di un importo marginale e resetta un conto alla rovescia di durata prestabilita. L'analisi microeconomica e strategica di tale meccanismo richiede la formalizzazione del Valore Atteso (Expected Value, $EV$), della soglia dinamica di pareggio (Breakeven Point) e dei meccanismi di contenimento delle deviazioni comportamentali derivanti dalla fallacia dei costi irrecuperabili (sunk cost fallacy).Il ciclo decisionale di un agente algoritmico si adatta dinamicamente allo stato dell'asta. Inizialmente, lo stato dell'asta viene valutato verificando se il valore atteso marginale sia non negativo e se la probabilità stimata di vincita superi la soglia critica. Se queste condizioni sono soddisfatte, l'algoritmo passa all'ottimizzazione del timing stocastico, attendendo che il conto alla rovescia raggiunga la finestra critica definita dalla latenza di rete e di server prima di inviare l'offerta. Qualora il valore atteso diventi negativo, il prezzo superi il livello massimo ammissibile o la spesa cumulata raggiunga la soglia di Stop-Loss preimpostata, l'algoritmo sterilizza i costi irrecuperabili ed esegue l'uscita immediata e definitiva dall'asta.Formulazione Rigorosa del Valore Atteso ($EV$)Si consideri un'asta al centesimo definita nello spazio discreto degli stati determinati dal numero totale di puntate effettuate dall'intero mercato, denotato con $t \in \mathbb{N}_0$. Si definiscono i seguenti parametri costitutivi:$V \in \mathbb{R}^+$: Valore reale o valore di stima di mercato dell'oggetto messo all'asta.$c \in \mathbb{R}^+$: Costo fisso e non rimborsabile della singola puntata.$v \in \mathbb{R}^+$: Incremento fisso del prezzo finale d'asta per singola puntata (tipicamente $v = 0{,}01$).$P_0 \ge 0$: Prezzo iniziale dell'asta ($P_0 = 0$).$P_t = P_0 + t \cdot v$: Prezzo nominale corrente dell'asta al tempo discreto $t$.$k_i \in \mathbb{N}_0$: Numero totale di puntate già piazziate dal giocatore generico $i$.$S_i(k_i) = k_i \cdot c$: Spesa cumulata sostenuta dal giocatore $i$ fino allo stato corrente.Sia $P_{\text{win}}(t)$ la probabilità condizionata che il rilancio effettuato dal giocatore $i$ allo stato $t$ risulti vincente, ovvero che nessun altro partecipante sottoscriva un'offerta aggiuntiva prima della scadenza del timer di conto alla rovescia.Nella teoria economica razionale e negli equilibri perfetti nei sottogiochi (Markov Perfect Equilibrium, MPE), i costi passati $S_i(k_i)$ costituiscono costi irrecuperabili (sunk costs) e non devono influenzare la funzione d'utilità attesa della decisione marginale. L'utilità dell'esito è modellata attraverso la funzione di valore atteso marginale $\mathbb{E}[\Delta U_i(t)]$:$$\mathbb{E}[\Delta U_i(t)] = P_{\text{win}}(t) \cdot \left[ V - (P_t + v) - c \right] + (1 - P_{\text{win}}(t)) \cdot (-c)$$Espandendo la combinazione lineare ed effettuando le dovute semplificazioni algebriche, si ottiene l'equazione canonica dell'Expected Value ($EV$) marginale per l'agente $i$:$$EV_i(t) = P_{\text{win}}(t) \cdot (V - P_t - v) - c$$L'equazione dimostra che l'utilità attesa di una nuova puntata è strettamente positiva se e solo se il prodotto tra la probabilità di vittoria e il margine di guadagno residuo espresso da $(V - P_t - v)$ eccede il costo marginale della puntata $c$.Equazioni del Breakeven Point DinamicoIl Breakeven Point dinamico definisce le condizioni limite in cui la puntata marginale presenta un valore atteso strettamente nullo ($EV_i(t) = 0$). Dalla condizione di indifferenza si ricavano le metriche strategiche fondamentali.Probabilità Minima di Vittoria ($P_{\text{win}}^*$)Per giustificare razionalmente il posizionamento di una puntata nello stato $t$, la probabilità stimata di aggiudicarsi l'asta deve soddisfare il vincolo di disuguaglianza:$$P_{\text{win}}(t) \ge P_{\text{win}}^*(t) \equiv \frac{c}{V - P_t - v}$$Se la stima della probabilità di vittoria offerta dal modello stocastico risulta inferiore al valore critico $P_{\text{win}}^*(t)$, l'azione ottimale nell'albero delle decisioni consiste nell'astensione dal rilancio.Prezzo Critico dell'Asta ($P_{\max}$) e Limite Teorico di Puntate ($t_{\max}$)Man mano che l'asta progredisce, $P_t$ aumenta, riducendo il dividendo di vincita espresso da $(V - P_t - v)$. L'asta raggiunge un punto di saturazione assoluta quando il margine residuo eguaglia il costo della singola puntata:$$V - P_{\max} - v = c \implies P_{\max} = V - c - v$$Da cui si evince che il numero totale massimo di puntate dell'asta $t_{\max}$, oltre il quale la partecipazione di un agente razionale è matematicamente preclusa per qualsiasi probabilità $P_{\text{win}} \le 1$, è dato da:$$t_{\max} = \left\lfloor \frac{V - c - v - P_0}{v} \right\rfloor$$Sterilizzazione della Sunk Cost Fallacy e Modello di Stop-LossStudi empirici basati su vaste basi dati di aste al centesimo dimostrano che i partecipanti umani deviano sistematicamente dall'equilibrio di razionalità pura, evidenziando una crescente propensione al rilancio all'aumentare della spesa cumulata $S_i(k_i)$. Tale fenomeno è formalizzato attraverso una funzione di utilità soggettiva distorta o "percepita" $U_i^{\text{percepita}}$:$$U_i^{\text{percepita}}(t; k_i) = P_{\text{win}}(t) \cdot \left[ V - (P_t + v) + \alpha \cdot S_i(k_i) \right] - c$$In questo contesto, $\alpha \in (0, 1]$ rappresenta il coefficiente di escalation irrazionale della fallacia dei costi irrecuperabili. Quando $\alpha > 0$, l'agente integra la perdita accumulata nel valore del premio, portando a sovra-offerte sistematiche e a rendimenti negativi.Per sterilizzare algoritmicamente questo bias comportamentale, il motore logico dell'algoritmo deve implementare un filtro strutturato di Stop-Loss Assoluto. Tale vincolo viene definito mediante una barriera assorbente nello spazio degli stati decisionale.Sia $B_0$ il budget totale allocato al bot e sia $\Phi \in (0, 1)$ il fattore di tolleranza al rischio (calibrato sul rapporto di Sharpe desiderato per il portafoglio dell'algoritmo). Si definisce il limite massimo di spesa individuale $S_{i,\max}$ per una singola asta come:$$S_{i,\max} = \min \left( \Phi \cdot V, \, B_0 \right)$$Il numero massimo di puntate consentite all'algoritmo nella specifica asta $i$, denotato con $k_{i,\max}$, è calcolato aprioristicamente mediante il pavimento intero:$$k_{i,\max} = \left\lfloor \frac{S_{i,\max}}{c} \right\rfloor$$L'indicatore logico di esecuzione $\mathbb{I}_{\text{bid}}(t)$ per l'algoritmo è vincolato dalla seguente funzione a gradino:$$\mathbb{I}_{\text{bid}}(t) = \begin{cases} 1, & \text{se } EV_i(t) \ge 0 \quad \land \quad k_i < k_{i,\max} \quad \land \quad P_t \le P_{\max} \\ 0, & \text{altrimenti} \end{cases}$$Attraverso tale formulazione, l'azzeramento di $\mathbb{I}_{\text{bid}}(t)$ isola il sistema da qualunque forma di escalation emotiva, garantendo la chiusura della posizione all'intersecarsi del vincolo di bilancio marginale.2. Ottimizzazione del Timing: Processi Stocastici, Catene di Markov e Latenza di ReteL'ottimizzazione dell'istante temporale nel quale inviare il comando di rilancio al server centrale è determinante per massimizzare la probabilità di vittoria e minimizzare il numero di puntate totali consumate dall'algoritmo.Modellizzazione dello Spazio degli Stati mediante Catene di MarkovL'asta al centesimo viene descritta come un processo stocastico a tempo continuo e spazio degli stati discreto, modellato tramite un processo Markoviano di Decisione (MDP). Sia $\tau \in [0, T_{\max}]$ il tempo residuo letto sul timer dell'asta (dove $T_{\max}$ è il tempo di reset, ad esempio 10 o 30 secondi).Lo stato del sistema al tempo continuo $\tau$ è espresso dal vettore:$$\mathcal{X}_\tau = \left( T_\tau, P_t, \mathbb{I}_{\text{lead}}, \boldsymbol{\lambda}_{\text{opp}} \right)$$In cui $T_\tau \in [0, T_{\max}]$ rappresenta la misura continua del timer di conto alla rovescia, $P_t$ è il prezzo corrente dell'asta, $\mathbb{I}_{\text{lead}} \in \{0, 1\}$ è la variabile binaria che indica se il bot è il leader temporaneo dell'asta ($\mathbb{I}_{\text{lead}} = 1$) o se lo è un avversario ($\mathbb{I}_{\text{lead}} = 0$), e $\boldsymbol{\lambda}_{\text{opp}}$ rappresenta il tasso cumulato di aggressività stocastica degli avversari attivi.Processo di Poisson Non Omogeneo per i Contro-Rilanci AvversariLa frequenza con cui la massa degli avversari deposita offerte al variare del timer $T_\tau$ segue un Processo di Poisson Non Omogeneo (NHPP). L'intensità temporale istantanea $\lambda(\tau)$ aumenta in modo esponenziale man mano che il timer si avvicina allo zero ($\tau \to 0$), a causa della concentrazione degli interventi degli utenti e degli altri bot nelle battute finali.La funzione di intensità $\lambda(\tau)$ è modellata come:$$\lambda(\tau) = \lambda_0 + \beta \cdot e^{-\gamma \tau}$$Dove $\lambda_0 > 0$ è il tasso di rilancio di base (offerte automatiche o casuali), $\beta > 0$ descrive la densità dei rilanci condizionati alla scadenza del timer e $\gamma > 0$ è il fattore di decadimento temporale.La probabilità che almeno un contro-rilancio avversario si verifichi all'interno dell'intervallo di tempo $[\tau_1, \tau_2]$ (con $\tau_2 < \tau_1$) è data dalla funzione di distribuzione cumulativa del processo di Poisson:$$\mathbb{P}(N(\tau_1, \tau_2) \ge 1) = 1 - \exp \left( -\int_{\tau_2}^{\tau_1} \lambda(s) \, ds \right)$$Ipotizzando un tasso di rilancio medio locale costante $\bar{\lambda}_{\tau}$ nell'intorno immediato dello scadere del timer, la probabilità che non vi sia alcun rilancio avversario nell'intervallo residuo di durata $\Delta \tau$ si semplifica in:$$\mathbb{P}(N(\Delta \tau) = 0) = e^{-\bar{\lambda}_{\tau} \cdot \Delta \tau}$$Integrazione della Finestra di Latenza ($\delta_L$) e Delays di ProcessamentoNella realtà operativa degli algoritmi di High-Frequency Bidding (HFB), il rilancio inviato al tempo $T_{\text{sent}}$ non viene registrato istantaneamente dall'architettura del server dell'asta. Si formalizza la variabile stocastica Latenza Totale di Rete e Server $\delta_{\text{tot}}$:$$\delta_{\text{tot}} = \delta_{\text{ping}} + \delta_{\text{proc}} + \delta_{\text{jitter}}$$I cui componenti sono così caratterizzati:$\delta_{\text{ping}}$: Tempo di propagazione dei pacchetti via socket TCP/WebSockets (Round Trip Time / 2).$\delta_{\text{proc}}$: Latenza interna di elaborazione del server centrale e accodamento della transazione.$\delta_{\text{jitter}}$: Variabile casuale distribuita secondo una Log-Normale $\ln(\delta_{\text{jitter}}) \sim \mathcal{N}(\mu_j, \sigma_j^2)$, che rappresenta le fluttuazioni di rete.Per evitare che il rilancio giunga al server dopo l'azzeramento del timer ($\tau \le 0$, causando la perdita automatica dell'asta se $\mathbb{I}_{\text{lead}} = 0$), l'algoritmo deve stabilire una soglia critica di sicurezza $T^*$. Per un livello di confidenza target $1 - \eta$ (es. $99{,}9\%$), $T^*$ è definito da:$$T^* = \mathbb{E}[\delta_{\text{ping}} + \delta_{\text{proc}}] + F_{\delta_{\text{jitter}}}^{-1}(1 - \eta)$$Dove $F_{\delta_{\text{jitter}}}^{-1}$ rappresenta la funzione quantile della distribuzione del jitter.Equazione di Bellman per l'Optimal StoppingIl problema di determinare la frazione esatta di secondo in cui inviare l'offerta rientra nella Teoria dell'Arresto Ottimale (Optimal Stopping Theory). Denotiamo con $W(\tau, \mathbb{I}_{\text{lead}})$ la funzione valore ottima data dallo stato corrente.Se il bot è il leader ($\mathbb{I}_{\text{lead}} = 1$), l'azione ottimale è sempre l'attesa passiva (non effettuare rilanci):$$W(\tau, 1) = e^{-\int_{0}^{\tau} \lambda(s)ds} \cdot (V - P_t) + \int_{0}^{\tau} \lambda(y) e^{-\int_{y}^{\tau} \lambda(s)ds} W(y, 0) \, dy$$Se il bot non è il leader ($\mathbb{I}_{\text{lead}} = 0$), deve scegliere tra continuare ad attendere che il timer scenda ulteriormente (al fine di massimizzare la probabilità che l'asta finisca senza che altri rilancino) o eseguire la puntata allo stato limite $T^*$. L'Equazione d'Invarianza di Bellman è definita come:$$W(\tau, 0) = \max \left\{ \text{Wait}(\tau), \, \text{Bid}(\tau) \right\}$$Le cui componenti di scelta ammettono la seguente formulazione matematica:$$\text{Bid}(\tau) = -c + e^{-\int_{0}^{\delta_{\text{tot}}} \lambda(s)ds} \cdot (V - P_t - v) + \left(1 - e^{-\int_{0}^{\delta_{\text{tot}}} \lambda(s)ds}\right) W(T_{\max}, 0)$$$$\text{Wait}(\tau) = \mathbb{E}_{\Delta \tau} \left[ W(\tau - \Delta \tau, 0) \mid N(\Delta \tau) = 0 \right] \cdot \mathbb{P}(N(\Delta \tau) = 0) + \text{Interruzione avversaria}$$L'istante d'arresto ottimale $\tau^*$ si verifica nell'istante esatto in cui il valore dell'attesa interseca il valore del rilancio marginale sotto vincolo di latenza:$$\tau^* = \max \left( T^*, \, \arg \max_{\tau} \left[ \mathbb{P}(N(\tau, T^*) = 0) \cdot (V - P_t - v) - c \right] \right)$$In un regime competitivo con elevata densità di utenti ($\lambda \to \infty$ per $\tau \to 0$), l'istante ottimo converge alla soglia della latenza limite: $\tau^* = T^*$. Rilanciare prima di $T^*$ riduce l'efficienza algoritmica, concedendo un intervallo temporale aggiuntivo agli avversari per reagire all'offerta.3. Considerazioni Avanzate e Teoria dei GiochiLa struttura dinamica delle aste al centesimo permette di modellarle come varianti estese della Guerra di Logoramento (War of Attrition) a informazioni incomplete, all'interno della quale si strutturano equilibri di Nash strategici.Modellizzazione come War of Attrition ed Equilibrio di Nash AsimmetricoIn un'asta al centesimo con $N$ partecipanti razionali e neutrali al rischio, la probabilità di vincita è endogena ed è determinata dalle strategie miste adottate dagli avversari.Equilibrio Simmetrico di Markov Perfect (SPNE/MPE)In un Markov Perfect Equilibrium simmetrico (Hinnosaar, 2016; Platt et al., 2013), ogni partecipante non-leader adotta una strategia mista continua tale da rendere gli altri partecipanti indifferenti tra il rilanciare e l'abbandonare l'asta.Sia $h(t)$ il tasso di azzardo aggregato (aggregate hazard rate), ovvero la probabilità complessiva che almeno un giocatore non-leader effettui una puntata allo stato $t$. Condizionatamente al fatto che l'asta continui oltre la seconda fase, l'equilibrio impone che il tasso aggregato $h(t)$ soddisfi la seguente identità:$$h(t) = \frac{c}{V - P_t} = \frac{c}{V - (P_0 + t \cdot v)}$$Sia $p_i(t)$ la probabilità individuale di rilancio del singolo partecipante non-leader $i$. Assumendo $N-1$ non-leader simmetrici, il tasso aggregato è correlato alle probabilità individuali dalla relazione:$$1 - h(t) = \prod_{i=1}^{N-1} (1 - p_i(t)) = (1 - p_i(t))^{N-1}$$Risolvendo per $p_i(t)$ tramite espansione algebrica:$$p_i(t) = 1 - \left( 1 - \frac{c}{V - P_t} \right)^{\frac{1}{N-1}}$$Attraverso l'approssimazione del primo ordine di Taylor per $\frac{c}{V - P_t} \ll 1$, si ricava la forma semplificata:$$p_i(t) \approx \frac{c}{(N-1)(V - P_t)}$$Equilibrio Asimmetrico con Valutazioni EterogeneeSe gli avversari presentano valutazioni eterogenee dell'oggetto $V_1 > V_2 > \dots > V_N$, il modello si traduce in un Equilibrio di Nash Asimmetrico. Il giocatore $i$ con la valutazione $V_i$ più alta abbandonerà l'asta per ultimo. Per mantenere il giocatore $j$ (con valore $V_j < V_i$) indifferente, il giocatore $i$ deve generare un tasso di azzardo $h_i(t)$ calcolato sulla valutazione dell'avversario $j$:$$h_i(t) = \frac{c_j}{V_j - P_t}$$Ciò dimostra che gli agenti con valutazioni inferiori vengono progressivamente estromessi dal supporto dell'equilibrio di strategia mista man mano che $P_t$ aumenta, lasciando in gara esclusivamente i soggetti caratterizzati da un'elevata valutazione o da un grado marcato di irrazionalità.Inferenza Bayesiana per la Stima del Budget Residuo degli AvversariGli avversari nel mondo reale operano sotto vincoli di bilancio discreti (pacchetti di crediti acquistati, ad esempio da 20, 50, 200 puntate). Un algoritmo ottimizzato deve continuamente aggiornare la propria stima Bayesiana sul budget residuo $B_j$ dell'avversario $j$ sulla base della frequenza e del timing dei suoi rilanci passati.Formalizzazione del Modello BayesianoSia $B_j \in \{0, 1, 2, \dots, B_{\max}\}$ la variabile casuale discreta indicante il numero di crediti rimanenti nel conto dell'avversario $j$.Distribuzione a Priori ($P(B_j)$): Inizializzata sulla base della distribuzione empirica di acquisto dei pacchetti crediti sulla piattaforma.Vettore delle Osservazioni ($\mathcal{O}_j^{(t)}$): Sequenza delle azioni registrate dall'avversario $j$ fino allo stato $t$:$$\mathcal{O}_j^{(t)} = \left\{ k_j(t), \, \bar{\tau}_j, \, \sigma_{\tau, j}^2, \, \mathbb{I}_{\text{auto}, j} \right\}$$
-Dove $k_j(t)$ è il numero di puntate effettuate da $j$ nell'asta corrente, $\bar{\tau}_j$ è il tempo medio di risposta sul timer, $\sigma_{\tau, j}^2$ è la varianza del timing e $\mathbb{I}_{\text{auto}, j}$ è la probabilità che $j$ stia utilizzando uno strumento di rilancio automatico.La regola di aggiornamento Bayesiano dell'Incertezza sul Budget è definita da:$$P(B_j = b \mid \mathcal{O}_j^{(t)}) = \frac{L(\mathcal{O}_j^{(t)} \mid B_j = b) \cdot P(B_j = b)}{\sum_{m=k_j(t)}^{B_{\max}} L(\mathcal{O}_j^{(t)} \mid B_j = m) \cdot P(B_j = m)}$$Verosimiglianza ($L$) basata sul Comportamento di PerduranzaLa funzione di verosimiglianza $L(\mathcal{O}_j^{(t)} \mid B_j = b)$ è costruita modellando la propensione dell'utente al proseguimento dell'asta via distribuzione Binomiale Negativa o tramite la funzione di sopravvivenza del modello di Cox. All'aumentare delle puntate piazzate $k_j(t)$, la massa di probabilità per valori di $b$ prossimi a zero aumenta:$$L(\mathcal{O}_j^{(t)} \mid B_j = b) \propto \exp\left( -\lambda_{\text{exit}} \cdot \max(0, b - k_j(t)) \right)$$Il Valore Atteso del Budget Residuo dell'Avversario $j$ allo stato $t$ si calcola come:$$\mathbb{E}[B_j \mid \mathcal{O}_j^{(t)}] = \sum_{b = k_j(t)}^{B_{\max}} (b - k_j(t)) \cdot P(B_j = b \mid \mathcal{O}_j^{(t)})$$Se $\sum_{j \neq i} \mathbb{E}[B_j \mid \mathcal{O}_j^{(t)}] > \frac{V - P_t}{c}$, la concorrenza aggregata possiede una capacità finanziaria sufficiente a spingere l'asta oltre il punto di redditività marginale, segnalando all'algoritmo la necessità di un'uscita anticipata dall'asta.Considerazioni Teoriche Avanzate e Variabili di Rischio EsclusiveL'integrazione di componenti comportamentali ed eterogeneità individuali estende i modelli di equilibri standard.Cumulative Prospect Theory (CPT) e Avversione alle PerditeL'utilità di un partecipante umano non è lineare, ma segue i dettami della Cumulative Prospect Theory (CPT). La funzione di valore $v_{\text{CPT}}(x)$ presenta una discontinuità nell'origine guidata dal fattore di avversione alle perdite $\lambda_{\text{loss}} \approx 2{,}25$:$$v_{\text{CPT}}(x) = \begin{cases} x^\eta, & \text{se } x \ge 0 \\ -\lambda_{\text{loss}} \cdot (-x)^\eta, & \text{se } x < 0 \end{cases} \quad (\text{con } \eta \approx 0{,}88)$$Nelle aste al centesimo, poiché il costo della puntata $c$ viene percepito come una perdita certa immediata, la reazione psicologica dell'utente umano consiste nel cercare di annullare la perdita percepita effettuando ulteriori rilanci per rincorrere il premio, modificando la probabilità soggettiva attraverso la funzione di ponderazione $w(p)$:$$w(p) = \frac{p^\gamma}{\left( p^\gamma + (1-p)^\gamma \right)^{1/\gamma}}$$La sovraponderazione delle basse probabilità di vincita ($w(p) \gg p$ per $p \to 0$) spiega empiricamente il prolungamento anomalo delle aste ben oltre il limite $P_{\max}$.Preferenze di Rischio CARA vs Risk-SeekingL'inclusione di funzioni d'utilità di tipo Constant Absolute Risk Aversion (CARA) permette di categorizzare la popolazione dei partecipanti:$$U_{\text{CARA}}(x) = \frac{1 - e^{-A x}}{A}$$In cui $A$ rappresenta il coefficiente di avversione assoluta al rischio:$A > 0$: Agente avverso al rischio. La partecipazione all'asta si interrompe precocemente.$A = 0$: Agente neutrale al rischio (modello MPE standard).$A < 0$: Agente propenso al rischio (Risk-Seeking). Platt et al. (2013) e Augenblick (2016) dimostrano che una frazione significativa di utenti manifesta $A < 0$, assimilando l'asta al centesimo a un'attività di gioco d'azzardo ludico con utilità derivata dal processo stesso di partecipazione.Identificazione dei Bot Nativi di Piattaforma (Auto-Bidders)Molte piattaforme integrano algoritmi automatici proprietari (ad esempio "Rilancio Automatico" o "Autobid"). Un motore logico ottimizzato deve distinguere se l'avversario $j$ è un utente umano o un processo deterministico.Si definisce il Test di Determinisimo Temporale: sia $\Delta \tau_{j, m} = \tau_{j, m} - \tau_{j, m-1}$ l'intervallo temporale di reazione dell'avversario $j$ alla perdita della leadership. Se la varianza dei tempi di reazione tende a zero e l'entropia del segnale si azzera:$$\lim_{m \to \infty} \text{Var}(\Delta \tau_{j, m}) < \varepsilon_{\text{bot}} \quad \land \quad \text{Entropy}(\Delta \tau_j) \approx 0$$L'avversario viene classificato come Bot Programmato ($\mathbb{I}_{\text{auto}, j} = 1$). Poiché un bot automatico risponde in maniera insensibile alla spesa ed è programmato per rilanciare fino all'esaurimento di una soglia preimpostata, la strategia ottimale per l'algoritmo consiste nel sospendere l'asta e rientrare esclusivamente nell'eventualità in cui la stima del budget residuo del bot azzeri la sua capacità finanziaria ($\mathbb{E}[B_{\text{bot}}] \to 0$).4. Sintesi Strategica e Matrice Operativa di DecisioneA integrazione dell'architettura analitica descritta, la tabella seguente formalizza la matrice dei parametri costitutivi del motore logico-matematico, declinata in termini di variabili, vincoli teorici e regole operative di arresto ed esecuzione.Parametro / VariabileSimbolo MatematicoDefinizione OperativaRegola Decisionale AlgoritmicaPrincipi Teorici di RiferimentoValore Reale / Mercato$V$Valore economico nominale o stima di mercato dell'oggettoLimite superiore teorico per la spesa totale aggregata dell'asta.Teoria del Valore RazionaleCosto Puntata$c$Costo marginale d'ingresso per singolo rilancioCosto certo sottratto all'utilità marginale ad ogni azione.All-Pay Auction DynamicsIncremento Prezzo$v$Aumento del prezzo finale per ogni offerta ($v = 0{,}01$)Modula la velocità di crescita di $P_t$ verso $P_{\max}$.Penny Auction RulesProbabilità Critica$P_{\text{win}}^*(t)$Probabilità minima condizionata per EV non negativoEffettuare il rilancio solo se $P_{\text{win}}(t) \ge \frac{c}{V - P_t - v}$.Markov Perfect Equilibrium (MPE)Soglia Latenza$T^*$Istante limite invio segnale di rilancio sul timerInviare la puntata a $\tau^* = T^* = \mathbb{E}[\delta_{\text{tot}}] + Q_{1-\eta}(\delta_{\text{jitter}})$.Processi Poissoniani e Arresto OttimaleStop-Loss Assoluto$S_{i,\max}$Massima spesa cumulata consentita per singola astaAbbandono immediato se $S_i(k_i) \ge S_{i,\max}$.Sterilizzazione Sunk Cost FallacyTasso di Azzardo$h(t)$Probabilità aggregata di rilancio avversario allo stato $t$Se $h(t) > \frac{c}{V - P_t}$, l'asta presenta sovraccumulo competitivo.Equilibrio di Nash / War of AttritionBudget Avversario$\mathbb{E}[B_j]$Stima Bayesiana dei crediti residui dell'avversario $j$Se $\sum \mathbb{E}[B_j] \cdot c > V - P_t$, sospendere le puntate.Inferenza Bayesiana e Record BreakingL'integrazione coordinata di queste formulazioni matematiche permette all'architettura software di formalizzare le decisioni di trading in ambiente ad alta frequenza. L'applicazione rigorosa del filtro di Stop-Loss neutralizza le deviazioni irrazionali legate alla sunk cost fallacy, consentendo all'algoritmo di operare esclusivamente entro margini di valore atteso matematicamente favorevoli. Parallelamente, la formalizzazione dello stop ottimale su base poissoniana condizionata dalla latenza garantisce che le risorse finanziarie vengano allocate con l'efficienza temporale massima. Infine, l'estensione del modello tramite inferenza Bayesiana e categorizzazione psicometrica degli avversari fornisce al sistema un vantaggio competitivo strutturale rispetto agli agenti umani e ai bot deterministici convenzionali.
-
-Nota:
-- Non usare librerie esterne per quando possibile, anche per quanto riguarda il machine learning
-- In alternativa al metodo descritto prima possiamo definire delle sessioni "di addestramento" e delle sessioni "reali" in cui la prima si concentra su log e raccolta dati mentre la seconda di concentra su velocità ed esecuzione portando al minimo raccolta dati ed esportazione
-
-Storico aste per analisi: C:\Users\alber\Downloads\AutoBidder\storico-aste-20260907-163509.json
-
-Fai domande se non sei sicuro di come procedere oppure se non sono stato abbastanza chiaro.
diff --git a/Mimante/Services/BidStrategyService.cs b/Mimante/Services/BidStrategyService.cs
index cbb27b3..1e03b33 100644
--- a/Mimante/Services/BidStrategyService.cs
+++ b/Mimante/Services/BidStrategyService.cs
@@ -233,7 +233,10 @@ namespace AutoBidder.Services
auction.LearnedUnansweredProbability = p;
auction.LearnedExpectedValue = ev;
- if (Ml.LearningService.IsReady(settings) && ev < 0)
+ 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 =
diff --git a/Mimante/Tests/MlModelBacktest.cs b/Mimante/Tests/MlModelBacktest.cs
index 7957def..64aef5d 100644
--- a/Mimante/Tests/MlModelBacktest.cs
+++ b/Mimante/Tests/MlModelBacktest.cs
@@ -1,8 +1,5 @@
using System;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text;
using AutoBidder.Ml;
using Xunit;
using Xunit.Abstractions;
@@ -10,22 +7,16 @@ using Xunit.Abstractions;
namespace AutoBidder.Tests;
///
-/// Valuta il modello sui dossier veri, con una divisione onesta nel tempo: si impara dai
-/// più vecchi e si giudica sui più recenti, che il modello non ha mai visto. Sul futuro,
-/// non su ciò che ha già letto.
+/// Valuta il modello sui dossier veri. La logica sta in ,
+/// condivisa con la scheda dell'applicazione: qui si aggiunge solo l'attivazione da
+/// variabili d'ambiente e la condizione minima.
///
-/// Si attiva solo con le variabili d'ambiente, come la rigiocata:
///
/// AUTOBIDDER_ML_DIR — cartella dei dossier;
/// AUTOBIDDER_ML_MAX — quanti file al massimo (0 = tutti);
-/// AUTOBIDDER_ML_OUT — dove scrivere il rapporto.
+/// AUTOBIDDER_ML_OUT — dove scrivere il rapporto;
+/// AUTOBIDDER_ML_ME — il nostro nome utente, per i dossier vecchi.
///
-///
-/// Cosa misura. Quanto il modello separa le puntate rimaste senza risposta
-/// dalle altre (sollevamento nella cima della classifica), se la probabilità è nella scala
-/// giusta (calibrazione per fasce), e — sulle nostre puntate vere — cosa avrebbe fatto il
-/// cancello del valore atteso: quante ne avrebbe fermate, e quante di quelle fermate erano
-/// invece vincenti.
///
public class MlModelBacktest
{
@@ -33,8 +24,6 @@ public class MlModelBacktest
public MlModelBacktest(ITestOutputHelper output) => _output = output;
- private sealed record Row(double P, bool Unanswered, bool Mine, double Ev);
-
[Fact]
public void Valuta_il_modello_sui_dossier_reali()
{
@@ -47,199 +36,22 @@ public class MlModelBacktest
}
var max = int.TryParse(Environment.GetEnvironmentVariable("AUTOBIDDER_ML_MAX"), out var m) ? m : 0;
-
- // Ordine per nome = ordine di data: i dossier cominciano con la data.
- var files = Directory.GetFiles(folder, "*.jsonl").OrderBy(Path.GetFileName, StringComparer.Ordinal).ToList();
- if (max > 0 && files.Count > max) files = files.Skip(files.Count - max).ToList();
-
- // Il nostro nome utente: i dossier scritti prima di oggi non lo portano
- // nell'intestazione, e senza non si sa quali puntate erano nostre.
var me = Environment.GetEnvironmentVariable("AUTOBIDDER_ML_ME");
- var sessions = new List();
- foreach (var f in files)
- {
- var s = DossierTrainingReader.ReadFile(f, me);
- if (s != null && s.IsUsable) sessions.Add(s);
- }
-
- var report = new StringBuilder();
- report.AppendLine($"Dossier letti: {files.Count:N0}, utilizzabili: {sessions.Count:N0}");
-
- if (sessions.Count < 20)
- {
- report.AppendLine("Troppo pochi dossier utilizzabili per una valutazione.");
- _output.WriteLine(report.ToString());
- return;
- }
-
- var split = (int)(sessions.Count * 0.7);
- var train = sessions.Take(split).ToList();
- var test = sessions.Skip(split).ToList();
-
- // Le variabili si costruiscono una volta sola: leggere i file è la parte lenta,
- // addestrare è veloce, e così si possono confrontare più configurazioni.
- var trainX = train.SelectMany(s => DossierTrainingReader.Examples(s))
- .Select(e => (X: BidFeatures.Build(e.Context), Y: e.Unanswered)).ToList();
- var testX = test.SelectMany(s => DossierTrainingReader.Examples(s).Select(e => (s, e)))
- .Select(t => (
- X: BidFeatures.Build(t.e.Context), Y: t.e.Unanswered, Mine: t.e.Mine,
- Margine: t.s.BuyNowPrice is > 0 ? t.s.BuyNowPrice.Value - t.e.Context.AuctionDepthCents / 100.0 - 0.01 : double.NaN))
- .ToList();
-
- var n = testX.Count;
- var positives = testX.Count(r => r.Y);
- var baseRate = (double)positives / n;
-
- report.AppendLine($"Addestramento: {train.Count:N0} aste, {trainX.Count:N0} puntate");
- report.AppendLine($"Valutazione : {test.Count:N0} aste, {n:N0} puntate, {positives:N0} senza risposta ({baseRate:P3})");
- report.AppendLine();
-
- // ── Confronto fra configurazioni ─────────────────────────────────
- //
- // Il modello deve separare (sollevamento) E stare nella scala giusta
- // (calibrazione): la probabilità entra in un valore atteso in euro. Si prova una
- // griglia piccola e si sceglie la configurazione meglio calibrata fra quelle che
- // separano davvero.
- const double costo = 0.20;
- // Prima griglia (passo × passate × media): il passo 0,01 in una passata era il
- // migliore, ma sovrastimava ancora di 1,6-1,8 volte e la media non aiutava. Il
- // sospetto è la regolarizzazione: con centinaia di migliaia di passi il
- // decadimento accumulato riduce i pesi a due terzi, e i pesi sono quasi tutti
- // negativi, quindi le probabilità salgono. Seconda griglia: passo × regolarizzazione.
- var configs = new List<(string Nome, double Lr, int Epochs, bool Avg, double L2)>();
- foreach (var lr in new[] { 0.01, 0.003 })
- foreach (var l2 in new[] { 1e-4, 1e-6, 0.0 })
- configs.Add(($"passo {lr:0.000}, L2 {l2:0.######}", lr, 1, false, l2));
-
- report.AppendLine("Configurazione | sollev. 5% | prevista/osservata (cima 10%) | prevista/osservata (tutte)");
- report.AppendLine("-------------------------------------+------------+-------------------------------+---------------------------");
-
- OnlineLogit? best = null; string bestName = ""; double bestScore = double.MaxValue; double bestLift = 0;
- foreach (var (nome, lr, ep, avg, l2) in configs)
- {
- var modello = new OnlineLogit(BidFeatures.Size) { LearningRate = lr, UseAveraging = avg, L2 = l2 };
- for (var e = 0; e < ep; e++) foreach (var (x, y) in trainX) modello.Update(x, y);
-
- var ps = testX.Select(r => modello.Predict(r.X)).ToArray();
- var order = Enumerable.Range(0, n).OrderByDescending(i => ps[i]).ToArray();
- var k5 = Math.Max(1, n / 20); var k10 = Math.Max(1, n / 10);
- var lift5 = order.Take(k5).Count(i => testX[i].Y) / (double)k5 / baseRate;
- var top10 = order.Take(k10).ToArray();
- var ratioTop = top10.Average(i => ps[i]) / Math.Max(1e-9, top10.Average(i => testX[i].Y ? 1.0 : 0.0));
- var ratioAll = ps.Average() / baseRate;
-
- report.AppendLine($"{nome,-37} | {lift5,9:N2}x | {ratioTop,29:N2} | {ratioAll,25:N2}");
-
- // Punteggio: distanza della scala da 1 (in logaritmo), solo se separa.
- var score = Math.Abs(Math.Log(Math.Max(1e-9, ratioTop))) + Math.Abs(Math.Log(Math.Max(1e-9, ratioAll)));
- if (lift5 > 2.0 && score < bestScore) { bestScore = score; best = modello; bestName = nome; bestLift = lift5; }
- }
- report.AppendLine();
-
- if (best == null)
- {
- report.AppendLine("Nessuna configurazione separa abbastanza (sollevamento al 5% > 2x).");
- _output.WriteLine(report.ToString());
- Assert.Fail("nessuna configurazione separa");
- return;
- }
-
- report.AppendLine($"Scelta: {bestName}");
- report.AppendLine();
-
- // ── Valutazione prequenziale: come nell'uso vero ─────────────────
- //
- // Il modello in produzione è online: quando decide su un'asta ha già imparato da
- // tutte quelle chiuse prima. Giudicarlo "congelato" sui vecchi e applicato ai nuovi
- // gli attribuisce una deriva che nell'uso non subisce. Qui ogni asta di valutazione
- // viene prima prevista, poi appresa, in ordine di tempo: è esattamente la sequenza
- // del motore. Le previsioni sono quelle calibrate, come le vede il motore.
- var model = best;
- var rows = new List();
- var perAsta = test.Select(s => DossierTrainingReader.Examples(s)
- .Select(e => (
- X: BidFeatures.Build(e.Context), Y: e.Unanswered, Mine: e.Mine,
- Margine: s.BuyNowPrice is > 0 ? s.BuyNowPrice.Value - e.Context.AuctionDepthCents / 100.0 - 0.01 : double.NaN))
- .ToList()).ToList();
-
- foreach (var esempi in perAsta)
- {
- foreach (var r in esempi)
- {
- var p = model.PredictCalibrated(r.X);
- rows.Add(new Row(p, r.Y, r.Mine, double.IsNaN(r.Margine) ? double.NaN : p * r.Margine - costo));
- }
- foreach (var r in esempi) model.Update(r.X, r.Y);
- }
-
- report.AppendLine($"Valutazione prequenziale (prevista poi appresa, asta per asta), fattore di calibrazione finale {model.CalibrationFactor:N2}:");
-
- var meanPos = rows.Where(r => r.Unanswered).Average(r => r.P);
- var meanNeg = rows.Where(r => !r.Unanswered).Average(r => r.P);
- report.AppendLine($"P media sulle puntate senza risposta : {meanPos:P3}");
- report.AppendLine($"P media sulle altre : {meanNeg:P3}");
- report.AppendLine($"rapporto : {meanPos / Math.Max(meanNeg, 1e-9):N2}x");
- report.AppendLine();
-
- // Sollevamento: fra le puntate a probabilità più alta, quante erano davvero finali.
- var sorted = rows.OrderByDescending(r => r.P).ToList();
- report.AppendLine("Sollevamento nella cima della classifica (precisione / tasso di base):");
- double liftTop5 = 0;
- foreach (var frac in new[] { 0.01, 0.05, 0.10, 0.25 })
- {
- var k = Math.Max(1, (int)(n * frac));
- var top = sorted.Take(k);
- var prec = top.Count(r => r.Unanswered) / (double)k;
- var lift = prec / baseRate;
- if (frac == 0.05) liftTop5 = lift;
- report.AppendLine($" cima {frac:P0}: {k,8:N0} puntate, precisione {prec:P2}, sollevamento {lift:N2}x");
- }
- report.AppendLine();
-
- // Calibrazione: la probabilità prevista deve somigliare a quella osservata.
- report.AppendLine("Calibrazione per fasce di probabilità prevista:");
- var edges = new[] { 0.0, 0.001, 0.0025, 0.005, 0.01, 0.02, 0.05, 1.0 };
- for (var i = 0; i < edges.Length - 1; i++)
- {
- var bin = rows.Where(r => r.P >= edges[i] && r.P < edges[i + 1]).ToList();
- if (bin.Count == 0) continue;
- report.AppendLine($" P in [{edges[i]:P2}, {edges[i + 1]:P2}): {bin.Count,8:N0} puntate, prevista {bin.Average(r => r.P):P3}, osservata {bin.Average(r => r.Unanswered ? 1.0 : 0.0):P3}");
- }
- report.AppendLine();
-
- // Le nostre puntate vere: cosa avrebbe fatto il cancello.
- var mie = rows.Where(r => r.Mine && !double.IsNaN(r.Ev)).ToList();
- if (mie.Count > 0)
- {
- var passate = mie.Where(r => r.Ev >= 0).ToList();
- var fermate = mie.Where(r => r.Ev < 0).ToList();
- report.AppendLine($"Le mie puntate nel periodo di valutazione: {mie.Count:N0}, di cui finali {mie.Count(r => r.Unanswered):N0}");
- report.AppendLine($" il cancello ne avrebbe lasciate passare {passate.Count:N0} (finali: {passate.Count(r => r.Unanswered):N0})");
- report.AppendLine($" e ne avrebbe fermate {fermate.Count:N0} (di cui finali, cioè vincenti perse: {fermate.Count(r => r.Unanswered):N0})");
- }
- else
- {
- report.AppendLine("Nessuna mia puntata nel periodo di valutazione.");
- }
- report.AppendLine();
-
- report.AppendLine("Pesi più forti (cosa ha imparato):");
- foreach (var (nome, peso) in BidFeatures.Names.Select((nm, i) => (nm, model.Weights[i])).OrderByDescending(t => Math.Abs(t.Item2)).Take(15))
- report.AppendLine($" {peso,+8:F3} {nome}");
-
- var text = report.ToString();
- _output.WriteLine(text);
+ var report = LearningEvaluator.Run(folder, max, me, _output.WriteLine);
+ _output.WriteLine(report.Text);
var outFile = Environment.GetEnvironmentVariable("AUTOBIDDER_ML_OUT");
if (!string.IsNullOrWhiteSpace(outFile))
{
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outFile))!);
- File.WriteAllText(outFile, text);
+ File.WriteAllText(outFile, report.Text);
}
- // La condizione minima perché il modello serva a qualcosa: nella cima della
- // classifica le puntate finali devono essere più frequenti che a caso.
- Assert.True(liftTop5 > 1.0, $"il modello non separa: sollevamento al 5% = {liftTop5:N2}");
+ if (!report.Enough) return;
+
+ // La condizione minima perche' il modello serva a qualcosa: nella cima della
+ // classifica le puntate finali devono essere piu' frequenti che a caso.
+ Assert.True(report.Lift5 > 1.0, $"il modello non separa: sollevamento al 5% = {report.Lift5:N2}");
}
}