diff --git a/Mimante/App.xaml.cs b/Mimante/App.xaml.cs index 9fd6226..8423086 100644 --- a/Mimante/App.xaml.cs +++ b/Mimante/App.xaml.cs @@ -26,6 +26,11 @@ namespace AutoBidder AppPaths.Configure(settings.DataFolder, settings.StatsFolder, settings.LogFolder); AppPaths.EnsureFolders(); + // La modalità della sessione si fissa qui e non cambia più fino alla chiusura: + // chi salva le impostazioni a metà sessione vede l'avviso di riavvio, non un + // motore che cambia comportamento sotto i piedi. + RuntimeProfile.Initialize(settings.SessionMode); + // Le versioni precedenti scrivevano parte dei dati in Roaming e parte in Local, // senza sottocartelle: senza questo passaggio storico e aste sparirebbero. AppPaths.MigrateLegacyFiles(); @@ -34,6 +39,8 @@ namespace AutoBidder // proprio ciò che non si riesce a leggere a video. TextLogService.SessionStarted(AppInfo.Version); TextLogService.PurgeOldLogs(); + TextLogService.App("INFO", $"[MODALITÀ] {RuntimeProfile.Describe()}"); + TextLogService.App("INFO", $"[PERCORSI] configurazione in {AppPaths.ConfigRoot}, dati in {AppPaths.DataFolder}"); // Le righe di registro delle aste confluiscono nei rispettivi dossier. AuctionDossier.CaptureAuctionLogs(); diff --git a/Mimante/Controls/SettingsControl.xaml b/Mimante/Controls/SettingsControl.xaml index 0510027..26c0be0 100644 --- a/Mimante/Controls/SettingsControl.xaml +++ b/Mimante/Controls/SettingsControl.xaml @@ -44,6 +44,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mimante/Controls/SettingsControl.xaml.cs b/Mimante/Controls/SettingsControl.xaml.cs index 5c61678..49b0d00 100644 --- a/Mimante/Controls/SettingsControl.xaml.cs +++ b/Mimante/Controls/SettingsControl.xaml.cs @@ -48,6 +48,30 @@ namespace AutoBidder.Controls Utilities.ThemeManager.SetAndSave(false); } + // ── Modalità della sessione ────────────────────────────────────── + + private void SessionModeRadio_Checked(object sender, RoutedEventArgs e) => + RefreshModeNotice(Utilities.RuntimeProfile.Mode); + + /// + /// Mostra che modalità è in vigore adesso, e l'avviso di riavvio solo se la scelta + /// nella scheda differisce da quella. Un avviso sempre acceso non lo legge nessuno. + /// + public void RefreshModeNotice(string modalitaInVigore) + { + if (ModeRestartNotice == null || ModeCurrentText == null) return; + + var scelta = ModeRaceRadio?.IsChecked == true + ? Utilities.RuntimeProfile.Gara + : Utilities.RuntimeProfile.Addestramento; + + ModeCurrentText.Text = $"In vigore adesso: {modalitaInVigore}"; + ModeRestartNotice.Visibility = + string.Equals(scelta, modalitaInVigore, System.StringComparison.OrdinalIgnoreCase) + ? Visibility.Collapsed + : Visibility.Visible; + } + // Non servono propriet� wrapper - MainWindow.xaml.cs accede direttamente ai controlli tramite: // Settings.DefaultBidBeforeDeadlineMsTextBox (definito nel XAML con x:Name) // Settings.MaxLogLinesPerAuctionTextBox (definito nel XAML con x:Name) diff --git a/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs b/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs index ccbb457..18b1fae 100644 --- a/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs +++ b/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Settings.cs @@ -107,6 +107,23 @@ namespace AutoBidder Settings.DetailedStatsCheckBox.IsChecked = settings.DetailedStatsEnabled; Settings.CatalogCacheSecondsTextBox.Text = settings.CatalogCacheSeconds.ToString(); + // Modalità: si mostra quella salvata; l'avviso compare se differisce da + // quella in vigore, che è stata fissata all'avvio e non cambia fino al riavvio. + if (string.Equals(settings.SessionMode, RuntimeProfile.Gara, StringComparison.OrdinalIgnoreCase)) + Settings.ModeRaceRadio.IsChecked = true; + else + Settings.ModeTrainingRadio.IsChecked = true; + Settings.RefreshModeNotice(RuntimeProfile.Mode); + + Settings.QuietHoursCheckBox.IsChecked = settings.QuietHoursEnabled; + Settings.QuietHoursStartTextBox.Text = settings.QuietHoursStart.ToString(); + Settings.QuietHoursEndTextBox.Text = settings.QuietHoursEnd.ToString(); + + Settings.AutoRemoveFinishedCheckBox.IsChecked = settings.AutoRemoveFinished; + Settings.AutoRemoveKeepMyBidsCheckBox.IsChecked = settings.AutoRemoveKeepWithMyBids; + Settings.AutoRemoveKeepWonCheckBox.IsChecked = settings.AutoRemoveKeepWon; + Settings.AutoRemoveKeepUnclearCheckBox.IsChecked = settings.AutoRemoveKeepUnclear; + Settings.WriteAppLogCheckBox.IsChecked = settings.WriteAppLogFile; Settings.WriteFreeBidsLogCheckBox.IsChecked = settings.WriteFreeBidsLogFile; Settings.WriteDossiersCheckBox.IsChecked = settings.WriteAuctionDossiers; @@ -413,6 +430,22 @@ namespace AutoBidder settings.DetailedStatsEnabled = Settings.DetailedStatsCheckBox.IsChecked ?? true; + settings.SessionMode = Settings.ModeRaceRadio.IsChecked == true + ? RuntimeProfile.Gara + : RuntimeProfile.Addestramento; + Settings.RefreshModeNotice(RuntimeProfile.Mode); + + settings.QuietHoursEnabled = Settings.QuietHoursCheckBox.IsChecked ?? true; + if (int.TryParse(Settings.QuietHoursStartTextBox.Text?.Trim(), out var qStart) && qStart is >= 0 and <= 23) + settings.QuietHoursStart = qStart; + if (int.TryParse(Settings.QuietHoursEndTextBox.Text?.Trim(), out var qEnd) && qEnd is >= 0 and <= 24) + settings.QuietHoursEnd = qEnd; + + settings.AutoRemoveFinished = Settings.AutoRemoveFinishedCheckBox.IsChecked ?? true; + settings.AutoRemoveKeepWithMyBids = Settings.AutoRemoveKeepMyBidsCheckBox.IsChecked ?? true; + settings.AutoRemoveKeepWon = Settings.AutoRemoveKeepWonCheckBox.IsChecked ?? true; + settings.AutoRemoveKeepUnclear = Settings.AutoRemoveKeepUnclearCheckBox.IsChecked ?? true; + settings.WriteAppLogFile = Settings.WriteAppLogCheckBox.IsChecked ?? true; settings.WriteFreeBidsLogFile = Settings.WriteFreeBidsLogCheckBox.IsChecked ?? true; settings.WriteAuctionDossiers = Settings.WriteDossiersCheckBox.IsChecked ?? true; diff --git a/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Stats.cs b/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Stats.cs index 839981b..e75ac14 100644 --- a/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Stats.cs +++ b/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Stats.cs @@ -70,7 +70,8 @@ namespace AutoBidder var detail = BuildDetail(auction, state, record); - if (settings.DetailedStatsEnabled) + // In Gara il dettaglio esteso non si scrive: vedi RuntimeProfile. + if (settings.DetailedStatsEnabled && RuntimeProfile.DetailedStats) { AuctionDetailStore.Append(detail); } diff --git a/Mimante/Core/MainWindow.Statistics.cs b/Mimante/Core/MainWindow.Statistics.cs index 45ab1f3..d1c4c71 100644 --- a/Mimante/Core/MainWindow.Statistics.cs +++ b/Mimante/Core/MainWindow.Statistics.cs @@ -105,11 +105,26 @@ namespace AutoBidder if (dialog.ShowDialog() != true) return; + // Le statistiche per prodotto sono un archivio a parte, alimentato asta per + // asta man mano che chiudono: dopo una pulizia descriverebbero aste che non ci + // sono più — e sono loro a decidere i limiti consigliati e i tetti di puntate. + // Si ricostruiscono da zero dallo storico appena pulito, l'unica fonte rimasta. + var ricostruite = 0; + try + { + ricostruite = ProductStatsStore.RebuildFromHistory(); + } + catch (Exception ex) + { + Log($"[STATISTICHE] Ricalcolo per prodotto non riuscito: {ex.Message}", LogLevel.Warning); + } + LoadStatistics(); LoadProducts(); - Log($"[STORICO] Pulizia: tolte {dialog.RemovedCount} aste. " + - $"Copia di sicurezza in {dialog.BackupPath}", LogLevel.Success); + Log($"[STORICO] Pulizia: tolte {dialog.RemovedCount} aste; statistiche per prodotto " + + $"ricalcolate su {ricostruite} aste rimaste. Copia di sicurezza in {dialog.BackupPath}", + LogLevel.Success); } /// diff --git a/Mimante/Dialogs/StatsCleanupDialog.xaml b/Mimante/Dialogs/StatsCleanupDialog.xaml index ea08136..25161d6 100644 --- a/Mimante/Dialogs/StatsCleanupDialog.xaml +++ b/Mimante/Dialogs/StatsCleanupDialog.xaml @@ -46,6 +46,49 @@ ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi." Checked="Option_Changed" Unchecked="Option_Changed"/> + + + + + + + + + + + + + + + + + + + + + + + + _records; + /// Una riga dell'elenco prodotti: chiave, etichetta con il conteggio, spunta. + private sealed class ProductRow + { + public string Key { get; init; } = ""; + public string Label { get; init; } = ""; + public bool Selected { get; set; } + } + + private readonly List _products; + public StatsCleanupDialog(List records) { InitializeComponent(); _records = records ?? new List(); + + // I prodotti presenti nello storico, dal più numeroso: è l'ordine in cui + // uno li cerca quando vuole fare spazio. + _products = _records + .Where(r => !string.IsNullOrWhiteSpace(r.ProductKey)) + .GroupBy(r => r.ProductKey, StringComparer.OrdinalIgnoreCase) + .Select(g => new ProductRow + { + Key = g.Key, + Label = $"{g.Select(r => r.Name).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) ?? g.Key} ({g.Count()})" + }) + .OrderByDescending(p => p.Label.Length > 0 ? _records.Count(r => string.Equals(r.ProductKey, p.Key, StringComparison.OrdinalIgnoreCase)) : 0) + .ToList(); + + ProductsList.ItemsSource = _products; + Loaded += (_, _) => Refresh(); } @@ -62,6 +88,22 @@ namespace AutoBidder.Dialogs options.OlderThan = DateTime.Now.AddDays(-days); } + options.RemoveObservedOnly = ChkObservedOnly.IsChecked == true; + options.RemoveWithMyBids = ChkWithMyBids.IsChecked == true; + + if (ChkPriceRatio.IsChecked == true && + double.TryParse(PriceRatioBox.Text.Trim().Replace(',', '.'), + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var ratio) && + ratio > 0) + { + options.MaxPriceRatioPercent = ratio; + } + + var scelti = _products.Where(p => p.Selected).Select(p => p.Key).ToList(); + if (scelti.Count > 0) + options.RemoveProductKeys = new HashSet(scelti, StringComparer.OrdinalIgnoreCase); + return options; } @@ -71,6 +113,14 @@ namespace AutoBidder.Dialogs { if (SummaryText == null) return; + if (ProductsHeader != null) + { + var n = _products.Count(p => p.Selected); + ProductsHeader.Text = n == 0 + ? $"Per prodotto: nessuno scelto ({_products.Count} nello storico)" + : $"Per prodotto: {n} scelti"; + } + var report = StatsMaintenance.Preview(_records, ReadOptions()); SummaryText.Text = report.Removed == 0 diff --git a/Mimante/MainWindow.xaml.cs b/Mimante/MainWindow.xaml.cs index 01ed062..c5b3ed4 100644 --- a/Mimante/MainWindow.xaml.cs +++ b/Mimante/MainWindow.xaml.cs @@ -273,14 +273,46 @@ namespace AutoBidder // ===== AUCTION MONITOR EVENT HANDLERS ===== + // ── Coalescenza degli aggiornamenti verso l'interfaccia ────────── + // + // Con il tetto delle richieste tolto ogni asta produce ~17 interrogazioni al + // secondo, e ognuna arrivava qui: un BeginInvoke, una scansione lineare dei + // ViewModel, e venti notifiche di proprietà. Con trenta aste sono diecimila + // notifiche al secondo sul thread dell'interfaccia, con cinquantasette quasi + // ventimila. È questo che rende l'applicazione lenta — non il disco, la cui + // scrittura era già asincrona e costa mezzo punto di CPU. + // + // La stragrande maggioranza di quei poll non porta niente di nuovo da mostrare: + // stesso prezzo, stesso puntatore, stesso stato, stesso secondo sul cronometro. + // Il conto alla rovescia scorre da solo grazie al battito da 200 ms, che legge la + // scadenza ancorata e non ha bisogno del poll. Quindi si manda all'interfaccia + // solo il poll in cui cambia qualcosa di visibile, e comunque uno al secondo per + // asta perché ping e contatori si rinfreschino. + // + // Il motore non è toccato: la decisione di puntare vede ogni poll come prima. + private readonly System.Collections.Concurrent.ConcurrentDictionary _ultimoInviato = new(); + + private static string FirmaVisibile(AuctionState s) => + string.Concat(s.Price.ToString("F2"), '|', s.LastBidder, '|', s.Status, '|', (int)s.Timer, '|', s.IsMyBid ? '1' : '0'); + private void AuctionMonitor_OnAuctionUpdated(AuctionState state) { + var firma = FirmaVisibile(state); + var adesso = Environment.TickCount64; + + if (_ultimoInviato.TryGetValue(state.AuctionId, out var prec) && + prec.Firma == firma && adesso - prec.Tick < 1000) + { + return; // niente di nuovo da mostrare, e l'ultimo rinfresco è recente + } + + _ultimoInviato[state.AuctionId] = (firma, adesso); + Dispatcher.BeginInvoke(() => { var vm = _auctionViewModels.FirstOrDefault(a => a.AuctionId == state.AuctionId); if (vm == null) return; - // Sempre: aggiorna la riga in griglia (poche proprietà, costo trascurabile). vm.UpdateState(state); // I pannelli di dettaglio mostrano SOLO l'asta selezionata: ricostruirli diff --git a/Mimante/Modifiche.txt b/Mimante/Modifiche.txt index 33baedc..ecc80b1 100644 --- a/Mimante/Modifiche.txt +++ b/Mimante/Modifiche.txt @@ -1,2 +1,19 @@ Ottimo! Ora altre modifiche: -- Aggiungi l'opzione di default già attiva che mi permette di cancellare dall'elenco un'asta terminata. Aggiungere altre opzioni per non eliminare quelle su cui ho puntato ma che sono terminate in modo da verificarle a mano +- 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 diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs index b6e514b..24817c6 100644 --- a/Mimante/Services/AuctionMonitor.cs +++ b/Mimante/Services/AuctionMonitor.cs @@ -452,7 +452,7 @@ namespace AutoBidder.Services // Il dossier registra tutto ciò che passa di qui: è materiale che esiste // solo adesso. var pollSettings = Settings; - AuctionDossier.OpenFor(auction, pollSettings)?.Poll(state, pollSettings.DossierIncludeRawPolls); + AuctionDossier.OpenFor(auction, pollSettings)?.Poll(state, pollSettings.DossierIncludeRawPolls && RuntimeProfile.RawPolls); if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0) { @@ -870,7 +870,7 @@ namespace AutoBidder.Services } else { - auction.AddLog($"✓ Convenienza OK (check={settings.ValueCheckEnabled}, buyNow={auction.BuyNowPrice?.ToString("F2") ?? "N/D"})", + if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Convenienza OK (check={settings.ValueCheckEnabled}, buyNow={auction.BuyNowPrice?.ToString("F2") ?? "N/D"})", Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Value); } @@ -925,7 +925,7 @@ namespace AutoBidder.Services } else if (session != null) { - auction.AddLog($"✓ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})", + if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})", Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit); } } @@ -933,7 +933,7 @@ namespace AutoBidder.Services // CONTROLLO 2: Non puntare se sono già il vincitore corrente if (state.IsMyBid) { - auction.AddLog($"✓ Sono già vincitore corrente - skip", + if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Sono già vincitore corrente - skip", Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.BidAttempt); return false; } @@ -954,7 +954,7 @@ namespace AutoBidder.Services if (maxBids > 0) { - auction.AddLog($"✓ Puntate OK ({usedBids}/{maxBids})", + if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Puntate OK ({usedBids}/{maxBids})", Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit); } } @@ -976,7 +976,7 @@ namespace AutoBidder.Services return false; } - auction.AddLog($"✓ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "∞")}])", + if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "∞")}])", Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Price); @@ -1020,7 +1020,7 @@ namespace AutoBidder.Services } } - auction.AddLog($"✓ Tutti i controlli ShouldBid superati", + if (RuntimeProfile.VerboseAuctionLog) auction.AddLog($"✓ Tutti i controlli ShouldBid superati", Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt); return true; diff --git a/Mimante/Tests/BidBudgetTests.cs b/Mimante/Tests/BidBudgetTests.cs index 04e2c16..506f51f 100644 --- a/Mimante/Tests/BidBudgetTests.cs +++ b/Mimante/Tests/BidBudgetTests.cs @@ -378,4 +378,87 @@ public class StatsMaintenanceTests Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys); } + + // ── Per prodotto, per esborso, per anomalia ─────────────────────────── + + private static CompletedAuctionRecord Con( + string id, string key, int mie = 0, double price = 1.00, double? value = 11.00) + { + var r = R(id: id, key: key, price: price, value: value, bidsFree: 1); + r.MyBidsUsed = mie; + return r; + } + + [Fact] + public void Toglie_solo_i_prodotti_scelti_senza_badare_alle_maiuscole() + { + var list = new List + { + Con("1", "buono a"), Con("2", "buono b"), Con("3", "buono a") + }; + + var options = new StatsMaintenance.CleanupOptions + { + RemoveProductKeys = new HashSet(new[] { "BUONO A" }, StringComparer.OrdinalIgnoreCase) + }; + + var kept = StatsMaintenance.Apply(list, options); + + Assert.Single(kept); + Assert.Equal("2", kept[0].AuctionId); + } + + [Fact] + public void Solo_osservate_tiene_quelle_con_mie_puntate() + { + var list = new List { Con("mia", "x", mie: 5), Con("vista", "x") }; + + var kept = StatsMaintenance.Apply(list, + new StatsMaintenance.CleanupOptions { RemoveObservedOnly = true }); + + Assert.Single(kept); + Assert.Equal("mia", kept[0].AuctionId); + } + + [Fact] + public void Con_mie_puntate_tiene_solo_il_mercato() + { + var list = new List { Con("mia", "x", mie: 5), Con("vista", "x") }; + + var kept = StatsMaintenance.Apply(list, + new StatsMaintenance.CleanupOptions { RemoveWithMyBids = true }); + + Assert.Single(kept); + Assert.Equal("vista", kept[0].AuctionId); + } + + [Fact] + public void Il_prezzo_fuori_scala_si_misura_sul_valore_e_senza_valore_non_si_indovina() + { + // 6 EUR su 10 di valore = 60%; 0,50 su 10 = 5%. + var list = new List + { + Con("cara", "x", price: 6.00, value: 10.00), + Con("normale", "x", price: 0.50, value: 10.00), + Con("senza valore", "x", price: 6.00, value: null), + }; + + var kept = StatsMaintenance.Apply(list, + new StatsMaintenance.CleanupOptions { MaxPriceRatioPercent = 40 }); + + Assert.Equal(2, kept.Count); + Assert.DoesNotContain(kept, r => r.AuctionId == "cara"); + Assert.Contains(kept, r => r.AuctionId == "senza valore"); + } + + [Fact] + public void Un_elenco_prodotti_vuoto_non_toglie_niente() + { + var list = new List { Con("1", "a"), Con("2", "b") }; + + var kept = StatsMaintenance.Apply(list, + new StatsMaintenance.CleanupOptions { RemoveProductKeys = new HashSet() }); + + Assert.Equal(2, kept.Count); + } } diff --git a/Mimante/Tests/RuntimeProfileTests.cs b/Mimante/Tests/RuntimeProfileTests.cs new file mode 100644 index 0000000..ab7cfe5 --- /dev/null +++ b/Mimante/Tests/RuntimeProfileTests.cs @@ -0,0 +1,72 @@ +using AutoBidder.Utilities; +using Xunit; + +namespace AutoBidder.Tests; + +/// +/// Modalita' della sessione. Due proprieta' da difendere: che Gara tolga esattamente il +/// lavoro dichiarato e nient'altro, e che la scelta sia immutabile per tutta la vita del +/// processo — un salvataggio a meta' sessione non deve cambiare il motore sotto i piedi. +/// +public class RuntimeProfileTests +{ + [Fact] + public void Addestramento_registra_tutto() + { + RuntimeProfile.ResetForTests(); + RuntimeProfile.Initialize(RuntimeProfile.Addestramento); + + Assert.False(RuntimeProfile.IsRace); + Assert.True(RuntimeProfile.RawPolls); + Assert.True(RuntimeProfile.VerboseAuctionLog); + Assert.True(RuntimeProfile.DetailedStats); + } + + [Fact] + public void Gara_toglie_solo_il_lavoro_dichiarato() + { + RuntimeProfile.ResetForTests(); + RuntimeProfile.Initialize(RuntimeProfile.Gara); + + Assert.True(RuntimeProfile.IsRace); + Assert.False(RuntimeProfile.RawPolls); + Assert.False(RuntimeProfile.VerboseAuctionLog); + Assert.False(RuntimeProfile.DetailedStats); + } + + [Fact] + public void La_prima_scelta_vince_per_tutta_la_sessione() + { + RuntimeProfile.ResetForTests(); + RuntimeProfile.Initialize(RuntimeProfile.Gara); + + // Un salvataggio delle impostazioni a meta' sessione passa di qui: non deve + // cambiare niente fino al riavvio. + RuntimeProfile.Initialize(RuntimeProfile.Addestramento); + + Assert.True(RuntimeProfile.IsRace); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("qualcosa")] + [InlineData("gara")] + public void Un_valore_ignoto_vale_Addestramento_e_le_maiuscole_non_contano(string? valore) + { + RuntimeProfile.ResetForTests(); + RuntimeProfile.Initialize(valore); + + // "gara" minuscolo e' Gara: un file modificato a mano non deve spegnere la scelta. + var atteso = string.Equals(valore, "gara", System.StringComparison.OrdinalIgnoreCase); + Assert.Equal(atteso, RuntimeProfile.IsRace); + } + + [Fact] + public void Il_predefinito_di_fabbrica_e_Addestramento() + { + RuntimeProfile.ResetForTests(); + Assert.Equal(RuntimeProfile.Addestramento, new AppSettings().SessionMode); + Assert.False(RuntimeProfile.IsRace); + } +} diff --git a/Mimante/Tests/TestEnvironment.cs b/Mimante/Tests/TestEnvironment.cs index d7ae777..19253a8 100644 --- a/Mimante/Tests/TestEnvironment.cs +++ b/Mimante/Tests/TestEnvironment.cs @@ -20,6 +20,10 @@ internal static class TestEnvironment { var sandbox = Path.Combine(Path.GetTempPath(), "AutoBidder.Tests", Guid.NewGuid().ToString("N")); + // Anche la radice della configurazione: senza, un test che tocca le impostazioni + // leggerebbe e scriverebbe quelle vere di chi lancia la suite. + AppPaths.OverrideConfigRootForTests(sandbox); + AppPaths.Configure( dataFolder: Path.Combine(sandbox, "Dati"), statsFolder: Path.Combine(sandbox, "Statistiche")); diff --git a/Mimante/Utilities/AppPaths.cs b/Mimante/Utilities/AppPaths.cs index 4d361aa..bb8b498 100644 --- a/Mimante/Utilities/AppPaths.cs +++ b/Mimante/Utilities/AppPaths.cs @@ -10,26 +10,104 @@ namespace AutoBidder.Utilities /// Local: le aste finivano in una cartella e le impostazioni in un'altra, cosa /// che rendeva difficile perfino sapere dove guardare per un salvataggio. /// - /// La struttura è deliberatamente su due livelli: + /// La struttura è deliberatamente su tre livelli: /// - /// Radice fissa (%LocalAppData%\AutoBidder): configurazione e - /// sessione. Non si sposta, perché è lì che si legge dove stanno gli altri - /// dati — spostarla creerebbe un problema dell'uovo e della gallina. La sessione - /// resta qui anche perché è protetta con DPAPI e legata a questo utente Windows: - /// su un disco condiviso non sarebbe comunque leggibile altrove. + /// Radice dell'autenticazione (%LocalAppData%\AutoBidder): sessione, + /// riferimenti del sito e profilo del browser. Non si sposta mai: la sessione è + /// protetta con DPAPI e legata a questo utente Windows, quindi su un altro disco o + /// in una cartella sincronizzata non sarebbe comunque leggibile. + /// Radice della configurazione (Documenti\AutoBidder per le + /// installazioni nuove): le impostazioni e, di serie, i dati. È una cartella nota a + /// Windows, quindi si trova senza dover leggere niente prima — che è l'unico requisito + /// per il file che dice dove stanno gli altri. /// Cartella dati (configurabile): aste monitorate, storico, prodotti - /// seguiti e statistiche. È questa che si può portare su un altro disco, in una - /// cartella sincronizzata o accanto ai propri fogli di calcolo. + /// seguiti e statistiche. Si può portare su un altro disco o accanto ai propri fogli + /// di calcolo. /// + /// + /// Chi ha già un'installazione non viene spostato. Se le impostazioni + /// esistono in %LocalAppData% e non in Documenti, si continua a usare quelle + /// dove sono: scelta esplicita dell'utente, per non toccare mesi di raccolta. Chi vuole + /// cambiare posto lo fa dalla scheda Impostazioni. /// public static class AppPaths { - /// Radice fissa: configurazione dell'applicazione e sessione. + /// + /// Radice dell'autenticazione: sessione, riferimenti del sito, profilo del browser. + /// Resta in %LocalAppData% qualunque cosa succeda alle altre cartelle. + /// public static string Root { get; } = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder"); - /// Impostazioni. Sempre nella radice fissa: contiene il percorso dei dati. - public static string SettingsFile => Path.Combine(Root, "settings.json"); + /// Radice della configurazione per chi installa da zero: Documenti. + private static string DocumentsRoot + { + get + { + try + { + var docs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + if (!string.IsNullOrWhiteSpace(docs)) return Path.Combine(docs, "AutoBidder"); + } + catch { /* profilo senza Documenti: si ripiega sotto */ } + + return Root; + } + } + + private static string? _configRootOverride; + private static string? _configRootResolved; + + /// + /// Radice della configurazione: dove stanno le impostazioni e, di serie, i dati. + /// + /// Si decide una volta sola, al primo accesso, e non cambia più per tutta la + /// vita del processo — spostarla a metà lascerebbe metà degli archivi in una + /// cartella e metà nell'altra. + /// + /// Ordine: Documenti se le impostazioni ci sono già; altrimenti + /// %LocalAppData% se ci sono lì (installazione precedente, che resta dov'è); + /// altrimenti Documenti, perché è un'installazione nuova. + /// + public static string ConfigRoot + { + get + { + if (_configRootOverride != null) return _configRootOverride; + return _configRootResolved ??= ResolveConfigRoot(); + } + } + + private static string ResolveConfigRoot() + { + var documenti = DocumentsRoot; + + if (File.Exists(Path.Combine(documenti, "settings.json"))) return documenti; + if (File.Exists(Path.Combine(Root, "settings.json"))) return Root; + + // Installazione nuova: Documenti, purché sia davvero scrivibile. + try + { + Directory.CreateDirectory(documenti); + return documenti; + } + catch + { + return Root; + } + } + + /// + /// Solo per i test: fissa la radice della configurazione senza guardare il disco. + /// Senza, la suite leggerebbe e scriverebbe le impostazioni vere di chi la lancia. + /// + public static void OverrideConfigRootForTests(string? path) => _configRootOverride = path; + + /// Impostazioni: nella radice della configurazione, che si trova senza leggerle. + public static string SettingsFile => Path.Combine(ConfigRoot, "settings.json"); + + /// Copie di sicurezza dello storico, fatte prima di ogni pulizia. + public static string BackupsFolder => Path.Combine(ConfigRoot, "Backups"); /// Cookie di sessione protetto con DPAPI. public static string SessionFile => Path.Combine(Root, "session.dat"); @@ -55,7 +133,7 @@ namespace AutoBidder.Utilities /// Cartella dei dati d'esercizio: aste monitorate, storico, prodotti seguiti. /// public static string DataFolder => - _dataFolderOverride.Length > 0 ? _dataFolderOverride : Path.Combine(Root, "Dati"); + _dataFolderOverride.Length > 0 ? _dataFolderOverride : Path.Combine(ConfigRoot, "Dati"); /// /// Cartella delle statistiche e delle esportazioni. Separata dalla precedente @@ -149,6 +227,7 @@ namespace AutoBidder.Utilities public static void EnsureFolders() { TryCreate(Root); + TryCreate(ConfigRoot); TryCreate(DataFolder); TryCreate(StatsFolder); TryCreate(LogFolder); diff --git a/Mimante/Utilities/CompletedAuctionsStore.cs b/Mimante/Utilities/CompletedAuctionsStore.cs index 046c4ed..6c1dc49 100644 --- a/Mimante/Utilities/CompletedAuctionsStore.cs +++ b/Mimante/Utilities/CompletedAuctionsStore.cs @@ -151,7 +151,7 @@ namespace AutoBidder.Utilities { lock (_lock) { - var cartella = Path.Combine(AppPaths.Root, "Backups"); + var cartella = AppPaths.BackupsFolder; Directory.CreateDirectory(cartella); var nome = $"completed-auctions_{DateTime.Now:yyyyMMdd_HHmmss}.json"; diff --git a/Mimante/Utilities/RuntimeProfile.cs b/Mimante/Utilities/RuntimeProfile.cs new file mode 100644 index 0000000..c44e8d2 --- /dev/null +++ b/Mimante/Utilities/RuntimeProfile.cs @@ -0,0 +1,83 @@ +using System; + +namespace AutoBidder.Utilities +{ + /// + /// La modalità della sessione, letta una volta all'avvio e immutabile fino alla chiusura. + /// + /// Addestramento registra tutto: ogni interrogazione nel dossier, ogni + /// decisione della strategia nel registro dell'asta, il dettaglio esteso delle aste + /// concluse. È la modalità con cui si raccolgono i dati che poi servono a tarare le + /// regole — e i dossier sono la materia prima di ogni analisi fatta finora. + /// + /// Gara fa solo il lavoro che serve a puntare. Quello che toglie: + /// + /// la riga JSON per ogni interrogazione nel dossier — a 17 poll al secondo per + /// asta un'asta lunga vale decine di megabyte; + /// le righe di registro informative («✓ prezzo OK» e simili), composte a ogni + /// giro prima ancora di sapere se qualcuno le leggerà; + /// il dettaglio esteso di ogni asta conclusa. + /// + /// Restano puntate, esiti, errori, lo storico delle aste concluse e tutto ciò che + /// alimenta le decisioni — il riconoscimento del duello con l'autopuntata compreso, che + /// vive sulle puntate avversarie e non sui poll. + /// + /// Cosa aspettarsi, misurato. Il lavoro che Gara evita costa 4,9 µs per + /// interrogazione (3,3 la riga JSON, 1,6 le sei stringhe di registro). A 57 aste per + /// 17 poll al secondo sono 4,7 ms di CPU al secondo: mezzo punto percentuale di un + /// core. Il guadagno vero di Gara è sul disco e sulla leggibilità dei dossier, non + /// sulla velocità: la scrittura era già asincrona, e la CPU spesa a prepararla è + /// trascurabile. Se il programma è lento, la causa sta altrove. + /// + /// Perché immutabile. Scelta esplicita dell'utente: la modalità vale per + /// tutta la sessione e cambiarla richiede il riavvio. Un interruttore a caldo + /// funzionerebbe uguale, ma una sessione tutta di un tipo ha un pregio suo — i dossier + /// che ne escono sono confrontabili fra loro, senza buchi a metà. + /// + public static class RuntimeProfile + { + public const string Addestramento = "Addestramento"; + public const string Gara = "Gara"; + + private static string? _mode; + + /// La modalità in vigore. Se nessuno l'ha fissata, Addestramento. + public static string Mode => _mode ?? Addestramento; + + public static bool IsRace => string.Equals(Mode, Gara, StringComparison.OrdinalIgnoreCase); + + /// + /// Fissa la modalità per tutta la vita del processo. La prima chiamata vince: le + /// successive sono ignorate, così un salvataggio delle impostazioni a metà sessione + /// non cambia il comportamento sotto i piedi del motore. + /// + public static void Initialize(string? mode) + { + if (_mode != null) return; + _mode = Normalize(mode); + } + + /// Il nome canonico di una modalità; qualunque altra cosa vale Addestramento. + public static string Normalize(string? mode) => + string.Equals(mode, Gara, StringComparison.OrdinalIgnoreCase) ? Gara : Addestramento; + + /// Solo per i test: azzera la scelta. + public static void ResetForTests() => _mode = null; + + // ── Cosa fa ciascuna modalità ──────────────────────────────────── + + /// Scrivere nel dossier una riga per ogni interrogazione. + public static bool RawPolls => !IsRace; + + /// Comporre le righe di registro informative («✓ prezzo OK» e simili). + public static bool VerboseAuctionLog => !IsRace; + + /// Scrivere il dettaglio esteso di ogni asta conclusa. + public static bool DetailedStats => !IsRace; + + /// Una riga per il registro d'avvio. + public static string Describe() => IsRace + ? "Gara: niente poll grezzi nel dossier, niente registro informativo, niente dettaglio esteso" + : "Addestramento: registrazione completa"; + } +} diff --git a/Mimante/Utilities/SettingsManager.cs b/Mimante/Utilities/SettingsManager.cs index 7c7c204..559f9f9 100644 --- a/Mimante/Utilities/SettingsManager.cs +++ b/Mimante/Utilities/SettingsManager.cs @@ -565,6 +565,13 @@ namespace AutoBidder.Utilities // Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il // doppio che fra le 10 e le 13. + /// + /// Modalità della sessione: Addestramento registra tutto, Gara fa solo + /// il lavoro che serve a puntare. Letta una volta all'avvio: cambiarla richiede il + /// riavvio. Vedi per cosa cambia e per quanto pesa. + /// + public string SessionMode { get; set; } = RuntimeProfile.Addestramento; + // ── Rimozione automatica delle aste concluse ───────────────────── // Vedi FinishedAuctionCleanup: si toglie di serie, si trattiene cio' su cui c'e' // stato un esborso. @@ -849,7 +856,7 @@ namespace AutoBidder.Utilities { // Le impostazioni stanno nella radice fissa: contengono il percorso degli altri // dati, quindi non possono a loro volta dipendere da quel percorso. - private static string _folder => AppPaths.Root; + private static string _folder => AppPaths.ConfigRoot; private static string _file => AppPaths.SettingsFile; private static readonly object _reloadLock = new(); diff --git a/Mimante/Utilities/StatsMaintenance.cs b/Mimante/Utilities/StatsMaintenance.cs index 44e6b23..b37fb86 100644 --- a/Mimante/Utilities/StatsMaintenance.cs +++ b/Mimante/Utilities/StatsMaintenance.cs @@ -113,6 +113,40 @@ namespace AutoBidder.Utilities /// Aste chiuse prima di questa data. Null = nessun limite. public DateTime? OlderThan { get; set; } + + // ── Per prodotto, per esborso, per anomalia ────────────────── + + /// + /// Chiavi prodotto da togliere per intero. Vuoto o null = nessun filtro. + /// Serve quando un prodotto è sparito dal sito o non interessa più: le sue + /// aste non alimentano nessuna decisione e pesano solo sulle medie globali. + /// Il confronto è senza distinzione di maiuscole: la chiave è già normalizzata, + /// ma un file modificato a mano può non esserlo. + /// + public HashSet? RemoveProductKeys { get; set; } + + /// + /// Togliere le aste solo osservate, cioè senza nessuna puntata nostra. Tiene + /// quelle su cui si è speso: sono le uniche col costo reale dentro. + /// + public bool RemoveObservedOnly { get; set; } + + /// + /// Togliere le aste su cui si è puntato. È il verso opposto: serve a chi vuole + /// uno storico «di mercato» non contaminato dalle proprie mosse. Un'asta in cui + /// si è intervenuti ha un prezzo finale che si è contribuito a fare, e per + /// stimare cosa succede senza di noi va tolta. + /// + public bool RemoveWithMyBids { get; set; } + + /// + /// Togliere le aste chiuse oltre questa percentuale del valore del prodotto. + /// Null = nessun limite. Una chiusura al 60% del valore su un buono da 100 € + /// non descrive il mercato: descrive due persone che si sono incaponite. + /// Tenerla sposta le medie e i limiti consigliati verso l'alto per tutti. + /// Senza valore noto l'asta resta: non si può dire, e non si indovina. + /// + public double? MaxPriceRatioPercent { get; set; } } /// Quante righe cadrebbero, e per quale motivo. @@ -183,6 +217,20 @@ namespace AutoBidder.Utilities if (options.OlderThan is { } limite && r.EndedAt < limite) Mark(r, "più vecchia del limite"); + + if (options.RemoveProductKeys is { Count: > 0 } chiavi && + !string.IsNullOrWhiteSpace(r.ProductKey) && chiavi.Contains(r.ProductKey)) + Mark(r, "prodotto scelto"); + + if (options.RemoveObservedOnly && r.MyBidsUsed <= 0) + Mark(r, "solo osservata"); + + if (options.RemoveWithMyBids && r.MyBidsUsed > 0) + Mark(r, "con mie puntate"); + + if (options.MaxPriceRatioPercent is { } tetto && r.BuyNowPrice is > 0 && + r.FinalPrice / r.BuyNowPrice.Value * 100.0 > tetto) + Mark(r, "prezzo finale fuori scala"); } return doomed;