diff --git a/Mimante/Models/AuctionInfo.cs b/Mimante/Models/AuctionInfo.cs index 2b9d4dc..1bc9349 100644 --- a/Mimante/Models/AuctionInfo.cs +++ b/Mimante/Models/AuctionInfo.cs @@ -65,10 +65,40 @@ namespace AutoBidder.Models [JsonPropertyName("BidsUsedOnThisAuction")] public int? BidsUsedOnThisAuction { get; set; } + // Timestamp public DateTime AddedAt { get; set; } = DateTime.UtcNow; public DateTime? LastClickAt { get; set; } + // ?? NUOVO: Sistema timing basato su deadline + /// + /// Timestamp UTC preciso della scadenza dell'asta. + /// Calcolato come: DateTime.UtcNow + Timer (quando riceviamo lo stato) + /// + [JsonIgnore] + public DateTime? DeadlineUtc { get; set; } + + /// + /// Timestamp UTC dell'ultimo aggiornamento della deadline. + /// Usato per rilevare reset del timer. + /// + [JsonIgnore] + public DateTime? LastDeadlineUpdateUtc { get; set; } + + /// + /// Timer raw dell'ultimo stato ricevuto (in secondi). + /// Usato per rilevare cambiamenti nel timer. + /// + [JsonIgnore] + public double LastRawTimer { get; set; } + + /// + /// True se la puntata gi stata schedulata per questo ciclo. + /// Resettato quando il timer si resetta. + /// + [JsonIgnore] + public bool BidScheduled { get; set; } + // Storico public List BidHistory { get; set; } = new List(); public Dictionary BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase); diff --git a/Mimante/Pages/AuctionBrowser.razor b/Mimante/Pages/AuctionBrowser.razor index 3c56bcd..14eeb05 100644 --- a/Mimante/Pages/AuctionBrowser.razor +++ b/Mimante/Pages/AuctionBrowser.razor @@ -1,4 +1,4 @@ -@page "/browser" +@page "/browser" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @using AutoBidder.Models @using AutoBidder.Services @@ -220,18 +220,18 @@
@auction.Name
- @auction.CurrentPrice.ToString("0.00") + @auction.CurrentPrice.ToString("0.00") € @if (auction.BuyNowPrice > 0) { - Compra: @auction.BuyNowPrice.ToString("0.00") + Compra: @auction.BuyNowPrice.ToString("0.00") € }
- @(string.IsNullOrEmpty(auction.LastBidder) ? "" : auction.LastBidder) + @(string.IsNullOrEmpty(auction.LastBidder) ? "—" : auction.LastBidder)
@@ -295,7 +295,14 @@ private List categories = new(); private List auctions = new(); private List filteredAuctions = new(); -private int selectedCategoryIndex = 0; + +// 🔥 Usa stato persistente da AppState +private int selectedCategoryIndex +{ + get => AppState.BrowserCategoryIndex; + set => AppState.BrowserCategoryIndex = value; +} + private int currentPage = 0; private bool isLoading = false; @@ -303,8 +310,12 @@ private bool isLoadingMore = false; private bool canLoadMore = true; private string? errorMessage = null; -// ? NUOVO: Ricerca -private string searchQuery = ""; +// 🔥 Usa stato persistente per la ricerca +private string searchQuery +{ + get => AppState.BrowserSearchQuery; + set => AppState.BrowserSearchQuery = value; +} private System.Threading.Timer? stateUpdateTimer; private CancellationTokenSource? cts; @@ -314,9 +325,20 @@ private bool isUpdatingInBackground = false; { await LoadCategories(); + // 🔥 Se c'è una categoria salvata, carica le aste if (categories.Count > 0) { - await LoadAuctions(); + // Se selectedCategoryIndex è valido, carica quella categoria + if (selectedCategoryIndex >= 0 && selectedCategoryIndex < categories.Count) + { + await LoadAuctions(); + } + else + { + // Altrimenti carica la prima categoria + selectedCategoryIndex = 0; + await LoadAuctions(); + } } // Auto-update states every 500ms for real-time price updates @@ -532,9 +554,30 @@ private bool isUpdatingInBackground = false; { if (browserAuction.IsMonitored) return; - // ?? Carica impostazioni di default + // 🔥 Carica impostazioni di default var settings = AutoBidder.Utilities.SettingsManager.Load(); + // 🔥 Determina stato iniziale da impostazioni + bool isActive = false; + bool isPaused = false; + + switch (settings.DefaultNewAuctionState) + { + case "Active": + isActive = true; + isPaused = false; + break; + case "Paused": + isActive = true; + isPaused = true; + break; + case "Stopped": + default: + isActive = false; + isPaused = false; + break; + } + var auctionInfo = new AuctionInfo { AuctionId = browserAuction.AuctionId, @@ -542,7 +585,7 @@ private bool isUpdatingInBackground = false; OriginalUrl = browserAuction.Url, BuyNowPrice = (double)browserAuction.BuyNowPrice, - // ?? FIX: Applica valori dalle impostazioni + // 🔥 Applica valori dalle impostazioni BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs, CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid, MinPrice = settings.DefaultMinPrice, @@ -550,8 +593,9 @@ private bool isUpdatingInBackground = false; MinResets = settings.DefaultMinResets, MaxResets = settings.DefaultMaxResets, - IsActive = true, - IsPaused = true, // Start paused + // 🔥 Usa stato da impostazioni invece di hardcoded + IsActive = isActive, + IsPaused = isPaused, AddedAt = DateTime.UtcNow }; @@ -565,7 +609,7 @@ private bool isUpdatingInBackground = false; // Save to disk AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList()); - // ?? FIX CRITICO: Avvia il monitor se non gi attivo + // ?? FIX CRITICO: Avvia il monitor se non è già attivo if (!AppState.IsMonitoringActive) { AuctionMonitor.Start(); diff --git a/Mimante/Pages/Settings.razor b/Mimante/Pages/Settings.razor index 40beaa9..816fd4a 100644 --- a/Mimante/Pages/Settings.razor +++ b/Mimante/Pages/Settings.razor @@ -181,82 +181,12 @@
-
Timing & Latenza
-
-
-
- - -
-
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
- - -
+
+ + Nota: Le strategie decidono SE puntare, non QUANDO. + Il timing controllato solo dall'impostazione "Anticipo puntata" nelle Impostazioni Predefinite.
-
Strategia Anti-AutoBid Bidoo
-
- - Come funziona: Bidoo ha un sistema di auto-puntata integrato che si attiva a ~2 secondi. - Aspettando che il timer scenda sotto questa soglia, lasciamo che gli utenti con auto-puntata attiva - puntino prima di noi, risparmiando puntate. -
-
-
-
- - -
-
-
- - -
Punta solo quando timer < questo valore (default: 1.8s)
-
-
-
- - -
-
-
-
Rilevamento Competizione
@@ -287,6 +217,27 @@
+
Controllo Convenienza
+
+
+
+ + +
+
+
+ + +
+ Negativo = tolleranza perdita. Es: -5 = permetti fino al 5% di perdita
+ Positivo = richiede risparmio. Es: 10 = richiedi almeno 10% di risparmio +
+
+
+
Soft Retreat
@@ -308,6 +259,7 @@
+
Puntata Probabilistica
@@ -421,25 +373,113 @@
- +

-
+
Cosa Loggare
+

Scegli quali informazioni visualizzare nei log delle aste.

+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
Debug Avanzato
+
+ + Attenzione: Queste opzioni generano molti log. Attiva solo per debug! +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
Limiti
+
+
-
+
-
+
diff --git a/Mimante/Services/ApplicationStateService.cs b/Mimante/Services/ApplicationStateService.cs index 2ce728d..4da96e1 100644 --- a/Mimante/Services/ApplicationStateService.cs +++ b/Mimante/Services/ApplicationStateService.cs @@ -148,6 +148,47 @@ namespace AutoBidder.Services } } + // === STATO AUCTION BROWSER === + + private int _browserCategoryIndex = 0; + private string _browserSearchQuery = ""; + + public int BrowserCategoryIndex + { + get + { + lock (_lock) + { + return _browserCategoryIndex; + } + } + set + { + lock (_lock) + { + _browserCategoryIndex = value; + } + } + } + + public string BrowserSearchQuery + { + get + { + lock (_lock) + { + return _browserSearchQuery; + } + } + set + { + lock (_lock) + { + _browserSearchQuery = value; + } + } + } + // === METODI GESTIONE ASTE === public void SetAuctions(List auctions) diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs index f47986d..a8b9439 100644 --- a/Mimante/Services/AuctionMonitor.cs +++ b/Mimante/Services/AuctionMonitor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -98,7 +98,7 @@ namespace AutoBidder.Services if (!_auctions.Any(a => a.AuctionId == auction.AuctionId)) { _auctions.Add(auction); - // ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con defaults e stato + // ? RIMOSSO: Log ridondante - viene gi loggato da MainWindow con defaults e stato // OnLog?.Invoke($"[+] Asta aggiunta: {auction.Name} (ID: {auction.AuctionId})"); } } @@ -111,20 +111,20 @@ namespace AutoBidder.Services var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId); if (auction != null) { - // ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla + // ?? Se l'asta terminata, salva le statistiche prima di rimuoverla if (!auction.IsActive && auction.LastState != null) { OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso..."); try { - // Determina se è stata vinta dall'utente + // Determina se stata vinta dall'utente var won = IsAuctionWonByUser(auction); OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}"); // Emetti evento per salvare le statistiche - // Questo trigger sarà gestito in Program.cs con scraping HTML + // Questo trigger sar gestito in Program.cs con scraping HTML OnAuctionCompleted?.Invoke(auction, auction.LastState, won); } catch (Exception ex) @@ -143,7 +143,7 @@ namespace AutoBidder.Services } /// - /// Determina se l'asta è stata vinta dall'utente corrente + /// Determina se l'asta stata vinta dall'utente corrente /// private bool IsAuctionWonByUser(AuctionInfo auction) { @@ -154,7 +154,7 @@ namespace AutoBidder.Services if (string.IsNullOrEmpty(username)) return false; - // Controlla se l'ultimo puntatore è l'utente + // Controlla se l'ultimo puntatore l'utente return auction.LastState.LastBidder?.Equals(username, StringComparison.OrdinalIgnoreCase) == true; } @@ -288,26 +288,41 @@ namespace AutoBidder.Services continue; } - // Delay adattivo OTTIMIZZATO basato su timer più basso - var lowestTimer = activeAuctions - .Select(a => GetLastTimer(a)) - .Where(t => t > 0) - .DefaultIfEmpty(999) - .Min(); - - int delayMs = lowestTimer switch + // ?? Delay adattivo ULTRA-OTTIMIZZATO + // Considera l'offset target pi basso tra tutte le aste attive + var settings = Utilities.SettingsManager.Load(); + + // ?? Polling VELOCE quando vicino alla scadenza + int minDelayMs = 500; + + foreach (var a in activeAuctions) { - < 0.5 => 10, // Ultra-critico: polling ogni 10ms - < 1 => 20, // Iper-veloce: polling ogni 20ms - < 2 => 50, // Ultra-veloce: polling ogni 50ms - < 3 => 100, // Molto veloce: polling ogni 100ms - < 5 => 150, // Veloce: polling ogni 150ms - < 10 => 300, // Medio: polling ogni 300ms - < 30 => 500, // Lento: polling ogni 500ms - _ => 1000 // Molto lento: polling ogni 1s - }; - - await Task.Delay(delayMs, token); + if (a.IsPaused || !a.LastDeadlineUpdateUtc.HasValue) continue; + + // Calcola tempo stimato rimanente + var elapsed = (DateTime.UtcNow - a.LastDeadlineUpdateUtc.Value).TotalMilliseconds; + double estimatedRemaining = a.LastRawTimer - elapsed; + + int offsetMs = a.BidBeforeDeadlineMs > 0 + ? a.BidBeforeDeadlineMs + : settings.DefaultBidBeforeDeadlineMs; + + double msToTarget = estimatedRemaining - offsetMs; + + // Polling pi veloce quando vicino al target + int delay; + if (msToTarget < 100) delay = 5; + else if (msToTarget < 300) delay = 10; + else if (msToTarget < 500) delay = 20; + else if (msToTarget < 1000) delay = 50; + else if (msToTarget < 2000) delay = 100; + else if (msToTarget < 5000) delay = 200; + else delay = 400; + + minDelayMs = Math.Min(minDelayMs, delay); + } + + await Task.Delay(minDelayMs, token); } catch (OperationCanceledException) { @@ -355,7 +370,7 @@ namespace AutoBidder.Services // ?? Aggiorna latenza con storico auction.AddLatencyMeasurement(state.PollingLatencyMs); - // ?? Segna tracking dall'inizio se è la prima volta + // ?? Segna tracking dall'inizio se la prima volta if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0) { auction.IsTrackedFromStart = true; @@ -368,7 +383,7 @@ namespace AutoBidder.Services MergeBidHistory(auction, state.RecentBidsHistory); } - // 🔥 FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio) + // ?? FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio) // Questo assicura che la puntata vincente sia sempre inclusa if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0) { @@ -391,7 +406,7 @@ namespace AutoBidder.Services var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var statePrice = (decimal)state.Price; - // Verifica se questa puntata non è già presente + // Verifica se questa puntata non gi presente var alreadyExists = auction.RecentBids.Any(b => Math.Abs(b.Price - statePrice) < 0.001m && b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase)); @@ -441,7 +456,7 @@ namespace AutoBidder.Services if (state.Status == AuctionStatus.Running) { - // Log RIMOSSO per ridurre verbosità - polling continuo non necessita log + // Log RIMOSSO per ridurre verbosit - polling continuo non necessita log // Solo eventi importanti (bid, reset, errori) vengono loggati } else if (state.Status == AuctionStatus.Paused) @@ -457,11 +472,15 @@ namespace AutoBidder.Services OnAuctionUpdated?.Invoke(state); UpdateAuctionHistory(auction, state); + + // ?? NUOVO: Calcola e aggiorna valore prodotto se disponibili dati if (auction.BuyNowPrice.HasValue || auction.ShippingCost.HasValue) { try { + var settings = Utilities.SettingsManager.Load(); + var productValue = Utilities.ProductValueCalculator.Calculate( auction, state.Price, @@ -470,9 +489,9 @@ namespace AutoBidder.Services auction.CalculatedValue = productValue; - // Log valore solo se cambia significativamente o ogni 10 polling + // Log valore solo se abilitato nelle impostazioni bool shouldLogValue = false; - if (auction.PollingLatencyMs % 10 == 0) // Ogni ~10 poll + if (settings.LogValueCalculations && auction.PollingLatencyMs % 10 == 0) // Ogni ~10 poll { shouldLogValue = true; } @@ -490,13 +509,10 @@ namespace AutoBidder.Services } } - // NUOVA LOGICA: Punta solo se siamo vicini alla deadline E nessun altro ha appena puntato - if (state.Status == AuctionStatus.Running && !auction.IsPaused && !auction.IsAttackInProgress) + // ?? NUOVA LOGICA: Controlla PRIMA il timing, POI le strategie + if (state.Status == AuctionStatus.Running && !auction.IsPaused) { - if (ShouldBid(auction, state)) - { - await ExecuteBidStrategy(auction, state, token); - } + await TryPlaceBid(auction, state, token); } } catch (Exception ex) @@ -507,116 +523,112 @@ namespace AutoBidder.Services } /// - /// Strategia di puntata SEMPLIFICATA - /// Le strategie restituiscono solo un booleano (posso puntare?). - /// Il timing è sempre fisso: offset globale o override per asta. + /// Sistema di puntata SEMPLICE. + /// Punta esattamente a `offset` ms dalla scadenza. + /// Le strategie decidono SE puntare, non QUANDO. /// - private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token) + private async Task TryPlaceBid(AuctionInfo auction, AuctionState state, CancellationToken token) { var settings = SettingsManager.Load(); - // Calcola il tempo rimanente in millisecondi - double timerMs = state.Timer * 1000; - - // 🛑 CONTROLLO: Se sono già il vincitore, non fare nulla - if (state.IsMyBid) - { - return; - } - - // 🎯 OFFSET FISSO: Usa override asta se impostato, altrimenti globale - int fixedOffsetMs = auction.BidBeforeDeadlineMs > 0 + // Offset: millisecondi prima della scadenza + int offsetMs = auction.BidBeforeDeadlineMs > 0 ? auction.BidBeforeDeadlineMs : settings.DefaultBidBeforeDeadlineMs; - // 🧠 VALUTAZIONE STRATEGIE (restituiscono solo bool) - var session = _apiClient.GetSession(); - var currentUsername = session?.Username ?? ""; + // Timer dall'API (in secondi, convertito in ms) + double timerMs = state.Timer * 1000; - // Verifica se le strategie permettono di puntare - var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername); + // Skip se gi vincitore o timer scaduto + if (state.IsMyBid || timerMs <= 0) return; - if (!decision.ShouldBid) + // ?? STIMA TEMPO RIMANENTE + // L'API d timer in secondi interi (1000, 2000, ecc.) + // Quando cambia, salvo il timestamp. Poi stimo localmente. + + bool timerChanged = Math.Abs(auction.LastRawTimer - timerMs) > 500; + + if (timerChanged || !auction.LastDeadlineUpdateUtc.HasValue) + { + auction.LastRawTimer = timerMs; + auction.LastDeadlineUpdateUtc = DateTime.UtcNow; + auction.BidScheduled = false; + } + + // Calcola tempo stimato rimanente + var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds; + double estimatedRemaining = timerMs - elapsed; + + // Log timing solo se abilitato + if (settings.LogTiming) + { + auction.AddLog($"[TIMING] API={timerMs:F0}ms, Elapsed={elapsed:F0}ms, Stima={estimatedRemaining:F0}ms"); + } + + // ?? il momento di puntare? + // LOGICA MIGLIORATA: Punta quando siamo nell'ultimo secondo E stima <= offset + // Ma con un fallback: se timer=1000ms e stima < 300ms, punta comunque! + // Questo evita di perdere l'asta per polling troppo lento + + bool shouldBidNow = false; + + if (timerMs <= 1000 && timerMs > 0) + { + // Siamo nell'ultimo secondo + if (estimatedRemaining <= offsetMs) + { + // Stima sotto l'offset: PUNTA! + shouldBidNow = true; + } + else if (estimatedRemaining <= 300) + { + // Fallback: stima < 300ms ma sopra offset? + // Punta comunque per sicurezza (meglio puntare "presto" che perdere l'asta) + shouldBidNow = true; + } + } + else if (estimatedRemaining <= offsetMs && estimatedRemaining > -200) + { + // Logica originale per timer > 1 secondo + shouldBidNow = true; + } + + if (!shouldBidNow) return; + + // Protezione doppia puntata + if (auction.BidScheduled) return; + + // Cooldown 1 secondo + if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) return; + + // ?? CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue) + if (!ShouldBid(auction, state)) { - auction.AddLog($"[STRATEGY] {decision.Reason}"); return; } - // 🕐 TIMER-BASED SCHEDULING (offset fisso, niente calcoli dinamici) - if (timerMs > fixedOffsetMs) + // ? MOMENTO GIUSTO! Verifica strategie avanzate + var session = _apiClient.GetSession(); + var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, session?.Username ?? ""); + + if (!decision.ShouldBid) { - // Timer ancora alto - Schedula puntata futura - double delayMs = timerMs - fixedOffsetMs; - - // Non schedulare se già c'è un task attivo per questa asta - if (auction.IsAttackInProgress) + if (settings.LogStrategyDecisions) { - return; + auction.AddLog($"[STRATEGY] {decision.Reason}"); } - - auction.IsAttackInProgress = true; - auction.LastUsedOffsetMs = fixedOffsetMs; - - auction.AddLog($"[TIMING] Timer={timerMs:F0}ms - Puntata tra {delayMs:F0}ms (offset fisso={fixedOffsetMs}ms)"); - - // Avvia task asincrono che attende e poi punta - _ = Task.Run(async () => - { - try - { - // Attendi il momento esatto - await Task.Delay((int)delayMs, token); - - // Verifica che l'asta sia ancora attiva e non in pausa - if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested) - { - auction.AddLog($"[TIMING] Task annullato (asta inattiva/pausa)"); - return; - } - - auction.AddLog($"[TIMING] Eseguo puntata!"); - - // Esegui la puntata - await ExecuteBid(auction, state, token); - } - catch (OperationCanceledException) - { - auction.AddLog($"[TIMING] Task cancellato"); - } - catch (Exception ex) - { - auction.AddLog($"[TIMING ERROR] {ex.Message}"); - } - finally - { - auction.IsAttackInProgress = false; - } - }, token); + return; } - else if (timerMs > 0 && timerMs <= fixedOffsetMs) + + // ?? PUNTA! + auction.BidScheduled = true; + + if (settings.LogBids) { - // Timer già nella finestra - Punta SUBITO senza delay - if (auction.IsAttackInProgress) - { - return; - } - - auction.IsAttackInProgress = true; - auction.LastUsedOffsetMs = fixedOffsetMs; - - try - { - auction.AddLog($"[TIMING] Timer in finestra ({timerMs:F0}ms <= {fixedOffsetMs}ms) - PUNTA SUBITO!"); - - // Esegui la puntata - await ExecuteBid(auction, state, token); - } - finally - { - auction.IsAttackInProgress = false; - } + auction.AddLog($"[BID] Puntata a ~{estimatedRemaining:F0}ms dalla scadenza"); } - // Se timer <= 0, asta già scaduta - Non fare nulla + + await ExecuteBid(auction, state, token); } /// @@ -639,7 +651,7 @@ namespace AutoBidder.Services _bidStrategy.RecordTimerExpired(auction); } - // 🔥 FIX: Aggiorna contatore puntate + // ?? FIX: Aggiorna contatore puntate if (result.Success) { if (result.RemainingBids.HasValue) @@ -681,7 +693,7 @@ namespace AutoBidder.Services Timer = state.Timer, LatencyMs = result.LatencyMs, Success = result.Success, - Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error + Notes = result.Success ? $"EUR{result.NewPrice:F2}" : (result.Error ?? "Errore sconosciuto") }); } catch (Exception ex) @@ -695,40 +707,33 @@ namespace AutoBidder.Services { var settings = Utilities.SettingsManager.Load(); - // ?? CONTROLLO ANTI-AUTOBID BIDOO (PRIORITÀ MASSIMA) - // Bidoo ha un sistema di auto-puntata che si attiva a ~2 secondi. - // Aspettiamo che il timer scenda sotto la soglia per lasciare che - // gli altri utenti con auto-puntata attiva puntino prima di noi. - // Questo ci fa risparmiare puntate perché non puntiamo "troppo presto". - if (settings.WaitForAutoBidEnabled && state.Timer > settings.WaitForAutoBidThresholdSeconds) + if (settings.LogTiming) { - // Timer ancora sopra la soglia - aspetta che le auto-puntate si attivino - if (settings.LogAutoBidWaitSkips) - { - auction.AddLog($"[AUTOBID] Timer {state.Timer:F2}s > soglia {settings.WaitForAutoBidThresholdSeconds}s - Aspetto auto-puntate Bidoo"); - } - return false; + auction.AddLog($"[DEBUG] === INIZIO CONTROLLI PUNTATA ==="); } - // ?? CONTROLLO 0: Verifica convenienza (se dati disponibili) - // IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0) - // Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate - if (auction.BuyNowPrice.HasValue && + // ?? CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili) + if (settings.ValueCheckEnabled && + auction.BuyNowPrice.HasValue && auction.BuyNowPrice.Value > 0 && auction.CalculatedValue != null && auction.CalculatedValue.Savings.HasValue && !auction.CalculatedValue.IsWorthIt) { - // Permetti comunque di puntare se il risparmio è ancora positivo (anche se piccolo) - // Blocca solo se sta andando in perdita significativa (< -5%) + // Usa la percentuale configurabile dall'utente if (auction.CalculatedValue.SavingsPercentage.HasValue && - auction.CalculatedValue.SavingsPercentage.Value < -5) + auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage) { - auction.AddLog($"[VALUE] Puntata bloccata: perdita {auction.CalculatedValue.SavingsPercentage.Value:F1}% (troppo costoso)"); + auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto"); return false; } } + if (settings.LogTiming && settings.ValueCheckEnabled) + { + auction.AddLog($"[DEBUG] ? Controllo convenienza OK"); + } + // ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate" // Se negli ultimi 10 secondi ci sono state 3+ puntate di utenti diversi, evita var recentBidsThreshold = 10; // secondi @@ -746,9 +751,14 @@ namespace AutoBidder.Services .Distinct(StringComparer.OrdinalIgnoreCase) .Count(); + if (settings.LogTiming) + { + auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}"); + } + if (activeBidders >= maxActiveBidders) { - // Controlla se l'ultimo bidder sono io - se sì, posso continuare + // Controlla se l'ultimo bidder sono io - se s, posso continuare var session = _apiClient.GetSession(); var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault(); @@ -762,6 +772,11 @@ namespace AutoBidder.Services } catch { /* Ignora errori nel controllo competizione */ } + if (settings.LogTiming) + { + auction.AddLog($"[DEBUG] ? Controllo competizione OK"); + } + // ?? CONTROLLO 1: Limite minimo puntate residue if (settings.MinimumRemainingBids > 0) { @@ -771,27 +786,44 @@ namespace AutoBidder.Services auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})"); return false; } + + if (settings.LogTiming && session != null) + { + auction.AddLog($"[DEBUG] ? Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})"); + } } - // ? CONTROLLO 2: Non puntare se sono già il vincitore corrente + // ? CONTROLLO 2: Non puntare se sono gi il vincitore corrente if (state.IsMyBid) { + if (settings.LogTiming) + { + auction.AddLog($"[DEBUG] Sono gi vincitore"); + } return false; } // ?? CONTROLLO 3: MinPrice/MaxPrice if (auction.MinPrice > 0 && state.Price < auction.MinPrice) { - auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}"); + auction.AddLog($"[PRICE] Prezzo troppo basso: {state.Price:F2} < Min {auction.MinPrice:F2}"); return false; } if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice) { - auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}"); + auction.AddLog($"[PRICE] Prezzo troppo alto: {state.Price:F2} > Max {auction.MaxPrice:F2}"); return false; } + if (settings.LogTiming) + { + if (auction.MinPrice > 0 || auction.MaxPrice > 0) + { + auction.AddLog($"[DEBUG] ? Range prezzo OK ({state.Price:F2} in [{auction.MinPrice:F2}, {auction.MaxPrice:F2}])"); + } + } + // ?? CONTROLLO 4: MinResets/MaxResets if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets) { @@ -805,16 +837,34 @@ namespace AutoBidder.Services return false; } + if (settings.LogTiming) + { + if (auction.MinResets > 0 || auction.MaxResets > 0) + { + auction.AddLog($"[DEBUG] ? Range reset OK ({auction.ResetCount} in [{auction.MinResets}, {auction.MaxResets}])"); + } + } + + // ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate) if (auction.LastClickAt.HasValue) { var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value; if (timeSinceLastClick.TotalMilliseconds < 800) { + if (settings.LogTiming) + { + auction.AddLog($"[DEBUG] Cooldown attivo ({timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms)"); + } return false; } } + if (settings.LogTiming) + { + auction.AddLog($"[DEBUG] === TUTTI I CONTROLLI SUPERATI ==="); + } + return true; } @@ -867,20 +917,8 @@ namespace AutoBidder.Services Notes = $"Puntata: EUR{state.Price:F2}" }); - // Aggiorna statistiche bidder - if (!string.IsNullOrEmpty(state.LastBidder)) - { - if (!auction.BidderStats.ContainsKey(state.LastBidder)) - { - auction.BidderStats[state.LastBidder] = new BidderInfo - { - Username = state.LastBidder - }; - } - - auction.BidderStats[state.LastBidder].BidCount++; - auction.BidderStats[state.LastBidder].LastBidTime = DateTime.UtcNow; - } + // ? RIMOSSO: Non incrementare qui - gi gestito da UpdateBidderStatsFromRecentBids + // L'incremento doppio causava conteggi gonfiati OnResetCountChanged?.Invoke(auction.AuctionId); } @@ -894,8 +932,8 @@ namespace AutoBidder.Services /// /// Unisce la storia puntate ricevuta dall'API con quella esistente, - /// mantenendo le puntate più vecchie e aggiungendo solo le nuove. - /// Le puntate sono ordinate con le più RECENTI in CIMA. + /// mantenendo le puntate pi vecchie e aggiungendo solo le nuove. + /// Le puntate sono ordinate con le pi RECENTI in CIMA. /// private void MergeBidHistory(AuctionInfo auction, List newBids) { @@ -908,7 +946,7 @@ namespace AutoBidder.Services // ?? FIX: Usa lock per thread-safety lock (auction.RecentBids) { - // Se la lista esistente è vuota, semplicemente copia le nuove + // Se la lista esistente vuota, semplicemente copia le nuove if (auction.RecentBids.Count == 0) { auction.RecentBids = newBids.ToList(); @@ -953,7 +991,7 @@ namespace AutoBidder.Services .ThenByDescending(b => b.Price) .ToList(); - // Limita al numero massimo di puntate (mantieni le più recenti = prime della lista) + // Limita al numero massimo di puntate (mantieni le pi recenti = prime della lista) if (maxEntries > 0 && auction.RecentBids.Count > maxEntries) { auction.RecentBids = auction.RecentBids @@ -975,7 +1013,7 @@ namespace AutoBidder.Services /// /// Assicura che la puntata corrente (quella vincente) sia sempre presente nello storico. /// Questo risolve il problema dell'API che a volte non include l'ultima puntata. - /// IMPORTANTE: Aggiunge solo se è una NUOVA puntata (prezzo/utente diverso dall'ultima registrata). + /// IMPORTANTE: Aggiunge solo se una NUOVA puntata (prezzo/utente diverso dall'ultima registrata). /// private void EnsureCurrentBidInHistory(AuctionInfo auction, AuctionState state) { @@ -984,22 +1022,18 @@ namespace AutoBidder.Services var statePrice = (decimal)state.Price; var currentBidder = state.LastBidder; - // 🔥 VERIFICA: Controlla se è la stessa puntata che abbiamo già in cima - // Evitiamo di aggiungere continuamente la stessa puntata ad ogni polling - if (auction.RecentBids.Count > 0) + // ?? VERIFICA: Controlla se questa puntata gi presente nella lista + // Evitiamo duplicati controllando prezzo + utente in TUTTA la lista + var alreadyExists = auction.RecentBids.Any(b => + Math.Abs(b.Price - statePrice) < 0.001m && + b.Username.Equals(currentBidder, StringComparison.OrdinalIgnoreCase)); + + if (alreadyExists) { - var topBid = auction.RecentBids[0]; // Prima = più recente - - // Se la puntata in cima è identica (stesso prezzo + stesso utente), salta - if (Math.Abs(topBid.Price - statePrice) < 0.001m && - topBid.Username.Equals(currentBidder, StringComparison.OrdinalIgnoreCase)) - { - return; // Già presente in cima, non serve aggiungere - } + return; // Gi presente, non serve aggiungere } - // 🔥 NUOVA PUNTATA: Aggiungi solo se diversa dall'ultima - // Questo significa che c'è stata una nuova puntata che l'API non ha ancora segnalato + // ?? NUOVA PUNTATA: Aggiungi solo se non esiste gi var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); auction.RecentBids.Insert(0, new BidHistoryEntry @@ -1010,52 +1044,45 @@ namespace AutoBidder.Services BidType = "Auto" }); - // Aggiorna anche le statistiche bidder - if (!auction.BidderStats.ContainsKey(currentBidder)) - { - auction.BidderStats[currentBidder] = new BidderInfo - { - Username = currentBidder, - BidCount = 1, - RecentBidCount = 1, - LastBidTime = DateTime.UtcNow - }; - } - else - { - var bidder = auction.BidderStats[currentBidder]; - bidder.BidCount++; - bidder.RecentBidCount++; - bidder.LastBidTime = DateTime.UtcNow; - } + // ? RIMOSSO: Non incrementare BidderStats qui + // gestito SOLO da UpdateBidderStatsFromRecentBids per evitare duplicazioni + // La puntata stata aggiunta a RecentBids, sar contata al prossimo aggiornamento } catch { /* Silenzioso */ } } /// - /// Aggiorna le statistiche dei bidder in modo CUMULATIVO. - /// BidCount è il totale dall'inizio del monitoraggio. - /// RecentBidCount è il conteggio nella finestra corrente di RecentBids. - /// I bidder NON vengono mai rimossi, il contatore è infinito. + /// Aggiorna le statistiche dei bidder basandosi SOLO su RecentBids. + /// QUESTO L'UNICO POSTO che aggiorna i conteggi. + /// IMPORTANTE: Il conteggio basato SOLO sulle puntate in RecentBids, non cumulativo. /// private void UpdateBidderStatsFromRecentBids(AuctionInfo auction) { try { - // Raggruppa puntate per username nella finestra corrente + // ?? FIX: Conta direttamente le puntate in RecentBids per ogni utente + // Non usare delta, conta sempre da zero basandosi sulla lista attuale var bidsByUser = auction.RecentBids .GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase) .ToDictionary( g => g.Key, g => new { - RecentCount = g.Count(), + Count = g.Count(), LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime }, StringComparer.OrdinalIgnoreCase ); - // Aggiorna o crea BidderInfo per ogni utente + // Resetta tutti i conteggi e ricalcola da zero + // Questo evita accumuli errati dovuti a delta sbagliati + foreach (var existing in auction.BidderStats.Values) + { + existing.BidCount = 0; + existing.RecentBidCount = 0; + } + + // Aggiorna con i conteggi reali da RecentBids foreach (var kvp in bidsByUser) { var username = kvp.Key; @@ -1063,37 +1090,22 @@ namespace AutoBidder.Services if (!auction.BidderStats.ContainsKey(username)) { - // Nuovo bidder - inizializza con i conteggi attuali auction.BidderStats[username] = new BidderInfo { Username = username, - BidCount = stats.RecentCount, // Primo conteggio - RecentBidCount = stats.RecentCount, + BidCount = stats.Count, + RecentBidCount = stats.Count, LastBidTime = stats.LastBidTime }; } else { var existing = auction.BidderStats[username]; - - // Calcola delta: quante nuove puntate rispetto all'ultimo check - int previousRecent = existing.RecentBidCount; - int currentRecent = stats.RecentCount; - - // Se il conteggio è aumentato, aggiungi la differenza al totale cumulativo - if (currentRecent > previousRecent) - { - existing.BidCount += (currentRecent - previousRecent); - } - - // Aggiorna conteggio finestra corrente e ultimo timestamp - existing.RecentBidCount = currentRecent; + existing.BidCount = stats.Count; + existing.RecentBidCount = stats.Count; existing.LastBidTime = stats.LastBidTime; } } - - // NON rimuovere i bidder che non sono più in RecentBids! - // Il conteggio totale è cumulativo e persistente. } catch (Exception ex) { diff --git a/Mimante/Services/BidStrategyService.cs b/Mimante/Services/BidStrategyService.cs index e295dae..6ac3019 100644 --- a/Mimante/Services/BidStrategyService.cs +++ b/Mimante/Services/BidStrategyService.cs @@ -17,84 +17,6 @@ namespace AutoBidder.Services private int _sessionTotalBids = 0; private DateTime _sessionStartedAt = DateTime.UtcNow; - /// - /// Calcola l'offset ottimale per una puntata considerando tutti i fattori - /// - public BidTimingResult CalculateOptimalTiming(AuctionInfo auction, AppSettings settings) - { - var result = new BidTimingResult - { - BaseOffsetMs = auction.BidBeforeDeadlineMs, - FinalOffsetMs = auction.BidBeforeDeadlineMs, - ShouldBid = true - }; - - // 1. Adaptive Latency Compensation - if (settings.AdaptiveLatencyEnabled) - { - result.LatencyCompensationMs = (int)auction.AverageLatencyMs; - result.FinalOffsetMs += result.LatencyCompensationMs; - } - - // 2. Dynamic Offset (basato su heat, storico, volatilit) - if (settings.DynamicOffsetEnabled) - { - var dynamicAdjustment = CalculateDynamicOffset(auction, settings); - result.DynamicAdjustmentMs = dynamicAdjustment; - result.FinalOffsetMs += dynamicAdjustment; - } - - // 3. Jitter (randomizzazione) - if (settings.JitterEnabled || (auction.JitterEnabledOverride ?? settings.JitterEnabled)) - { - result.JitterMs = _random.Next(-settings.JitterRangeMs, settings.JitterRangeMs + 1); - result.FinalOffsetMs += result.JitterMs; - } - - // 4. Clamp ai limiti - result.FinalOffsetMs = Math.Clamp(result.FinalOffsetMs, settings.MinimumOffsetMs, settings.MaximumOffsetMs); - - // Salva offset calcolato nell'asta - auction.DynamicOffsetMs = result.FinalOffsetMs; - - return result; - } - - /// - /// Calcola offset dinamico basato su heat, storico e volatilit - /// - private int CalculateDynamicOffset(AuctionInfo auction, AppSettings settings) - { - int adjustment = 0; - - // Pi l'asta "calda", pi anticipo serve - if (auction.HeatMetric > 50) - { - adjustment += (auction.HeatMetric - 50) / 2; // +0-25ms per heat 50-100 - } - - // Se ci sono state collisioni recenti, anticipa di pi - if (auction.ConsecutiveCollisions > 0) - { - adjustment += auction.ConsecutiveCollisions * 10; // +10ms per ogni collisione - } - - // Se la latenza volatile (alta deviazione), aggiungi margine - if (auction.LatencyHistory.Count >= 5) - { - var avg = auction.LatencyHistory.Average(); - var variance = auction.LatencyHistory.Sum(x => Math.Pow(x - avg, 2)) / auction.LatencyHistory.Count; - var stdDev = Math.Sqrt(variance); - - if (stdDev > 20) // Alta variabilit - { - adjustment += (int)(stdDev / 2); - } - } - - return adjustment; - } - /// /// Aggiorna heat metric per un'asta /// @@ -360,9 +282,43 @@ namespace AutoBidder.Services } } + // ? RIMOSSO: DetectLastSecondSniper - causava falsi positivi + // In un duello, TUTTI i bidder hanno pattern regolari (ogni reset del timer) + // Questa strategia bloccava puntate legittime e faceva perdere aste + + // ?? 7. STRATEGIA: Price Momentum (con soglia pi alta) + // Se il prezzo sta salendo TROPPO velocemente, pausa + var priceVelocity = CalculatePriceVelocity(auction); + if (priceVelocity > 0.10) // +10 centesimi/secondo = MOLTO veloce + { + decision.ShouldBid = false; + decision.Reason = $"Prezzo sale troppo veloce ({priceVelocity:F3}/s)"; + return decision; + } + return decision; } + /// + /// Calcola la velocit di crescita del prezzo (/secondo) + /// + private double CalculatePriceVelocity(AuctionInfo auction) + { + if (auction.RecentBids.Count < 5) return 0; + + var recentBids = auction.RecentBids.Take(10).ToList(); + if (recentBids.Count < 2) return 0; + + var first = recentBids.Last(); + var last = recentBids.First(); + + var timeDiffSeconds = last.Timestamp - first.Timestamp; + if (timeDiffSeconds <= 0) return 0; + + var priceDiff = last.Price - first.Price; + return (double)priceDiff / timeDiffSeconds; + } + /// /// Rileva pattern bot analizzando i delta timing degli ultimi bid /// diff --git a/Mimante/Utilities/ProductValueCalculator.cs b/Mimante/Utilities/ProductValueCalculator.cs index ca8ef7c..302ce24 100644 --- a/Mimante/Utilities/ProductValueCalculator.cs +++ b/Mimante/Utilities/ProductValueCalculator.cs @@ -41,7 +41,7 @@ namespace AutoBidder.Utilities } // Calcola risparmio rispetto al prezzo "Compra Subito" - if (auctionInfo.BuyNowPrice.HasValue) + if (auctionInfo.BuyNowPrice.HasValue && auctionInfo.BuyNowPrice.Value > 0) { var buyNowTotal = auctionInfo.BuyNowPrice.Value; if (auctionInfo.ShippingCost.HasValue) @@ -50,12 +50,24 @@ namespace AutoBidder.Utilities } value.Savings = buyNowTotal - value.TotalCostIfWin; - value.SavingsPercentage = (value.Savings.Value / buyNowTotal) * 100.0; + + // ?? FIX: Evita divisione per zero + if (buyNowTotal > 0) + { + value.SavingsPercentage = (value.Savings.Value / buyNowTotal) * 100.0; + } + else + { + // Se il buyNowTotal 0, imposta un valore fittizio negativo per indicare perdita + value.SavingsPercentage = -100.0; + } + value.IsWorthIt = value.Savings.Value > 0; } else { - // Senza prezzo "Compra Subito", consideriamo sempre conveniente + // Senza prezzo "Compra Subito" valido, consideriamo sempre conveniente + // Questo permette di puntare su aste senza dati di riferimento value.IsWorthIt = true; } diff --git a/Mimante/Utilities/SettingsManager.cs b/Mimante/Utilities/SettingsManager.cs index c07797b..a98e405 100644 --- a/Mimante/Utilities/SettingsManager.cs +++ b/Mimante/Utilities/SettingsManager.cs @@ -7,7 +7,7 @@ namespace AutoBidder.Utilities public class AppSettings { // NUOVE IMPOSTAZIONI PREDEFINITE PER LE ASTE - public int DefaultBidBeforeDeadlineMs { get; set; } = 200; + public int DefaultBidBeforeDeadlineMs { get; set; } = 800; public bool DefaultCheckAuctionOpenBeforeBid { get; set; } = false; public double DefaultMinPrice { get; set; } = 0; public double DefaultMaxPrice { get; set; } = 0; @@ -104,71 +104,64 @@ namespace AutoBidder.Utilities // STRATEGIE AVANZATE DI PUNTATA // ??????????????????????????????????????????????????????????????? + // ❌ RIMOSSO: Jitter, Offset Dinamico, Latenza Adattiva + // Il timing è gestito SOLO da DefaultBidBeforeDeadlineMs + // Le strategie decidono SE puntare, non QUANDO + + // 🎯 LOGGING GRANULARE + // ??????????????????????????????????????????????????????????????? + /// - /// Abilita compensazione adattiva della latenza. - /// Misura latenza reale per ogni asta e adatta l'anticipo automaticamente. + /// Log quando viene piazzata una puntata [BID] /// Default: true /// - public bool AdaptiveLatencyEnabled { get; set; } = true; + public bool LogBids { get; set; } = true; /// - /// Abilita jitter casuale sull'offset per evitare sincronizzazione con altri bot. - /// Aggiunge ±JitterRangeMs al timing di puntata. + /// Log quando una strategia blocca la puntata [STRATEGY] /// Default: true /// - public bool JitterEnabled { get; set; } = true; + public bool LogStrategyDecisions { get; set; } = true; /// - /// Range massimo del jitter casuale in millisecondi (±X ms). - /// Default: 50 (range -50ms a +50ms) + /// Log calcoli valore prodotto [VALUE] + /// Default: false (attiva per debug) /// - public int JitterRangeMs { get; set; } = 50; + public bool LogValueCalculations { get; set; } = false; /// - /// Abilita offset dinamico per asta basato su ping, storico e volatilità. + /// Log rilevamento competizione e heat [COMPETITION] + /// Default: false + /// + public bool LogCompetition { get; set; } = false; + + /// + /// Log timing e polling (molto verbose!) [TIMING] + /// Default: false (attiva solo per debug timing) + /// + public bool LogTiming { get; set; } = false; + + /// + /// Log errori e warning [ERROR/WARN] /// Default: true /// - public bool DynamicOffsetEnabled { get; set; } = true; + public bool LogErrors { get; set; } = true; /// - /// Offset minimo garantito in ms (non scende mai sotto questo valore). - /// Default: 80 - /// - public int MinimumOffsetMs { get; set; } = 80; - - /// - /// Offset massimo in ms (non supera mai questo valore). - /// Default: 500 - /// - public int MaximumOffsetMs { get; set; } = 500; - - // ?? STRATEGIA ANTI-AUTOBID BIDOO - // Bidoo ha un sistema di auto-puntata integrato che si attiva a ~2 secondi. - // Aspettando che questa soglia venga superata, lasciamo che gli altri - // utenti con auto-puntata attiva puntino prima di noi, risparmiando puntate. - - /// - /// Abilita la strategia di attesa per le auto-puntate di Bidoo. - /// Se true, aspetta che il timer scenda sotto la soglia prima di puntare. + /// Log stato asta (terminata, reset, ecc.) [STATUS] /// Default: true /// - public bool WaitForAutoBidEnabled { get; set; } = true; + public bool LogAuctionStatus { get; set; } = true; /// - /// Soglia in secondi sotto la quale si può puntare. - /// Bidoo attiva le auto-puntate a ~2 secondi, quindi aspettiamo che passino. - /// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate) + /// Log profiling avversari [OPPONENT] + /// Default: false /// - public double WaitForAutoBidThresholdSeconds { get; set; } = 1.8; - - /// - /// Se true, logga quando salta una puntata per aspettare le auto-puntate. - /// Default: false (per evitare spam nel log) - /// - public bool LogAutoBidWaitSkips { get; set; } = false; + public bool LogOpponentProfiling { get; set; } = false; // 🎯 STRATEGIE SEMPLIFICATE + /// /// Entry Point: Usato SOLO per calcolare i limiti consigliati (70% del MaxPrice storico). /// NON blocca le puntate! I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI. @@ -190,6 +183,26 @@ namespace AutoBidder.Utilities /// public bool UserExhaustionEnabled { get; set; } = true; + // 🎯 CONTROLLO CONVENIENZA PRODOTTO + + /// + /// Abilita il controllo di convenienza basato sul valore del prodotto. + /// Se attivo, blocca le puntate quando il costo totale supera il prezzo "Compra Subito" + /// di una percentuale superiore a MinSavingsPercentage. + /// Default: true + /// + public bool ValueCheckEnabled { get; set; } = true; + + /// + /// Percentuale minima di risparmio richiesta per continuare a puntare. + /// Valori negativi = tolleranza alla perdita. + /// Es: -5 = permetti fino al 5% di perdita rispetto al "Compra Subito" + /// 0 = blocca se costa uguale o più del "Compra Subito" + /// 10 = richiedi almeno 10% di risparmio + /// Default: -5 (permetti fino al 5% di perdita) + /// + public double MinSavingsPercentage { get; set; } = -5.0; + // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥 // RILEVAMENTO COMPETIZIONE E HEAT METRIC // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥