Compare commits

...

2 Commits

Author SHA1 Message Date
e18a09e1da Gestione massiva limiti prodotto e ottimizzazione ticker
Aggiunta barra azioni per gestione massiva limiti prodotto in Statistics.razor (applica, salva, attiva/disattiva, copia consigliati). Uniformati simboli euro e messaggi in italiano. Ottimizzata la logica del ticker: controllo puntata ora avviene prima del polling, gestione fine asta differita tramite PendingEndState. Introdotto controllo esplicito su MaxClicks per asta. Implementata cache delle impostazioni in SettingsManager per ridurre accessi disco. Vari fix minori e miglioramenti di robustezza.
2026-03-03 08:53:38 +01:00
f3262a0497 Log aste strutturato, limiti prodotto e UI statistiche
- Log per-asta ora strutturato con livelli, categorie e deduplicazione; motivi di blocco puntata tracciati in modo dettagliato e throttled
- Nuova visualizzazione log compatta e colorata nella UI
- Migliorate statistiche prodotto: aggiunta mediana prezzo, flag UseCustomLimits e editing limiti inline
- Impostazione priorità limiti nuove aste (globali vs personalizzati)
- Refactoring: rimossi limiti reset, UI statistiche rinnovata, ordinamenti e filtri avanzati
- Aggiornato schema DB (MedianFinalPrice, UseCustomLimits)
- Diagnostica periodica e log dettagliato su ticker/controlli
2026-02-16 23:10:04 +01:00
13 changed files with 2016 additions and 1215 deletions

View File

@@ -40,10 +40,10 @@ namespace AutoBidder.Models
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
/// <summary>
/// [OBSOLETO] Numero massimo di puntate consentite - Non più utilizzato nell'UI
/// Mantenuto per retrocompatibilità con salvataggi JSON esistenti
/// Numero massimo di puntate consentite per questa asta (0 = illimitato).
/// Impostato dall'utente nella griglia statistiche o dai limiti prodotto.
/// Controllato in ShouldBid contro BidsUsedOnThisAuction.
/// </summary>
[Obsolete("MaxClicks non è più utilizzato. Usa invece la logica di limiti per prodotto.")]
[JsonPropertyName("MaxClicks")]
public int MaxClicks { get; set; } = 0;
@@ -107,6 +107,13 @@ namespace AutoBidder.Models
[JsonIgnore]
public double LastScheduledTimerMs { get; set; }
/// <summary>
/// Stato di fine asta ricevuto dal poll ma non ancora processato.
/// Il ticker ha un'ultima occasione di puntare prima che venga gestito.
/// </summary>
[JsonIgnore]
public AuctionState? PendingEndState { get; set; }
// Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
@@ -118,9 +125,9 @@ namespace AutoBidder.Models
[JsonPropertyName("RecentBids")]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>();
// Log per-asta (non serializzato)
// Log per-asta strutturato (non serializzato)
[System.Text.Json.Serialization.JsonIgnore]
public List<string> AuctionLog { get; set; } = new();
public List<AuctionLogEntry> AuctionLog { get; set; } = new();
// Flag runtime: indica che è in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore]
@@ -168,64 +175,141 @@ namespace AutoBidder.Models
/// <summary>
/// Aggiunge una voce al log dell'asta con deduplicazione e limite automatico di righe.
/// Se il messaggio è identico all'ultimo, incrementa un contatore invece di duplicare.
/// Aggiunge una voce strutturata al log dell'asta con deduplicazione e limite righe.
/// Parsifica automaticamente il tag [TAG] per determinare livello e categoria.
/// </summary>
/// <param name="message">Messaggio da aggiungere al log</param>
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
public void AddLog(string message, int maxLines = 500)
public void AddLog(string message, int maxLines = 200)
{
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
// Protezione null-safety (dopo ClearData)
if (AuctionLog == null) AuctionLog = new();
// DEBUG: Print per verificare che i log vengano aggiunti
#if DEBUG
System.Diagnostics.Debug.WriteLine($"[AddLog] {AuctionId}: {message}");
#endif
var now = DateTime.Now;
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
// Parsifica tag dal messaggio per determinare livello e categoria
var (level, category, cleanMessage) = ParseLogTag(message);
// DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
if (AuctionLog.Count > 0)
{
var lastEntry = AuctionLog[^1]; // Ultimo elemento
// Estrai il messaggio senza timestamp e contatore
var lastMessageStart = lastEntry.IndexOf(" - ");
if (lastMessageStart > 0)
var last = AuctionLog[^1];
if (last.Message == cleanMessage && last.Category == category)
{
var lastMessage = lastEntry.Substring(lastMessageStart + 3);
// Rimuovi eventuale contatore esistente (es: " (x5)")
var counterMatch = System.Text.RegularExpressions.Regex.Match(lastMessage, @" \(x(\d+)\)$");
if (counterMatch.Success)
{
lastMessage = lastMessage.Substring(0, lastMessage.Length - counterMatch.Length);
}
// Se il messaggio è identico, aggiorna contatore
if (lastMessage == message)
{
int newCount = counterMatch.Success
? int.Parse(counterMatch.Groups[1].Value) + 1
: 2;
// Aggiorna l'ultimo entry con il nuovo contatore
AuctionLog[^1] = $"{timestamp} - {message} (x{newCount})";
return;
}
last.RepeatCount++;
last.Timestamp = now;
return;
}
}
// Nuovo messaggio diverso dall'ultimo
var entry = $"{timestamp} - {message}";
AuctionLog.Add(entry);
AuctionLog.Add(new AuctionLogEntry
{
Timestamp = now,
Level = level,
Category = category,
Message = cleanMessage
});
// Mantieni solo gli ultimi maxLines log
if (AuctionLog.Count > maxLines)
{
int excessCount = AuctionLog.Count - maxLines;
AuctionLog.RemoveRange(0, excessCount);
AuctionLog.RemoveRange(0, AuctionLog.Count - maxLines);
}
}
/// <summary>
/// Aggiunge una voce tipizzata al log dell'asta (senza parsing del tag).
/// </summary>
public void AddLog(string message, AuctionLogLevel level, AuctionLogCategory category)
{
// Protezione null-safety (dopo ClearData)
if (AuctionLog == null) AuctionLog = new();
var now = DateTime.Now;
if (AuctionLog.Count > 0)
{
var last = AuctionLog[^1];
if (last.Message == message && last.Category == category)
{
last.RepeatCount++;
last.Timestamp = now;
return;
}
}
AuctionLog.Add(new AuctionLogEntry
{
Timestamp = now,
Level = level,
Category = category,
Message = message
});
if (AuctionLog.Count > MAX_LOG_LINES)
{
AuctionLog.RemoveRange(0, AuctionLog.Count - MAX_LOG_LINES);
}
}
/// <summary>
/// Parsifica i tag [TAG] per determinare livello e categoria automaticamente.
/// </summary>
private static (AuctionLogLevel level, AuctionLogCategory category, string cleanMessage) ParseLogTag(string message)
{
// Cerca pattern [TAG] all'inizio del messaggio
var tagMatch = System.Text.RegularExpressions.Regex.Match(message, @"^\[([A-Z_ ]+)\]\s*(.*)$");
if (!tagMatch.Success)
return (AuctionLogLevel.Info, AuctionLogCategory.General, message);
var tag = tagMatch.Groups[1].Value.Trim();
var cleanMsg = tagMatch.Groups[2].Value;
return tag switch
{
// Bid/puntata
"BID" => (AuctionLogLevel.Bid, AuctionLogCategory.BidAttempt, cleanMsg),
"BID OK" => (AuctionLogLevel.Success, AuctionLogCategory.BidResult, cleanMsg),
"BID FAIL" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg),
"BID EXCEPTION" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg),
"MANUAL BID" => (AuctionLogLevel.Bid, AuctionLogCategory.BidAttempt, cleanMsg),
"MANUAL BID OK" => (AuctionLogLevel.Success, AuctionLogCategory.BidResult, cleanMsg),
"MANUAL BID FAIL" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg),
// Timing
"TICKER" => (AuctionLogLevel.Timing, AuctionLogCategory.Ticker, cleanMsg),
"TIMING" or "\u26a0\ufe0f TIMING" => (AuctionLogLevel.Warning, AuctionLogCategory.Ticker, cleanMsg),
// Prezzi/limiti
"PRICE" => (AuctionLogLevel.Warning, AuctionLogCategory.Price, cleanMsg),
"VALUE" => (AuctionLogLevel.Warning, AuctionLogCategory.Value, cleanMsg),
"LIMIT" => (AuctionLogLevel.Warning, AuctionLogCategory.Limit, cleanMsg),
// Reset
var r when r.StartsWith("RESET") => (AuctionLogLevel.Info, AuctionLogCategory.Reset, cleanMsg),
// Strategie
"STRATEGY" => (AuctionLogLevel.Strategy, AuctionLogCategory.Strategy, cleanMsg),
"COMPETITION" => (AuctionLogLevel.Strategy, AuctionLogCategory.Competition, cleanMsg),
// Diagnostica
"DIAG" => (AuctionLogLevel.Debug, AuctionLogCategory.Diagnostic, cleanMsg),
"DEBUG" => (AuctionLogLevel.Debug, AuctionLogCategory.General, cleanMsg),
// Stato
"START" => (AuctionLogLevel.Info, AuctionLogCategory.Status, cleanMsg),
"ASTA TERMINATA" => (AuctionLogLevel.Warning, AuctionLogCategory.Status, cleanMsg),
"\u26a0\ufe0f SUGGERIMENTO" => (AuctionLogLevel.Warning, AuctionLogCategory.Ticker, cleanMsg),
// Polling
"POLL ERROR" => (AuctionLogLevel.Error, AuctionLogCategory.Polling, cleanMsg),
// Errori generici
"ERROR" or "ERRORE" => (AuctionLogLevel.Error, AuctionLogCategory.General, cleanMsg),
"WARN" => (AuctionLogLevel.Warning, AuctionLogCategory.General, cleanMsg),
"OK" => (AuctionLogLevel.Success, AuctionLogCategory.General, cleanMsg),
_ => (AuctionLogLevel.Info, AuctionLogCategory.General, message)
};
}
public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms
// ???????????????????????????????????????????????????????????????
@@ -428,6 +512,7 @@ namespace AutoBidder.Models
// Pulisci oggetti complessi
LastState = null;
PendingEndState = null;
CalculatedValue = null;
DuelOpponent = null;
WinLimitDescription = null;

View File

@@ -0,0 +1,126 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Entry strutturata per il log di una singola asta.
/// Contiene timestamp preciso, livello di gravità, categoria e messaggio.
/// </summary>
public class AuctionLogEntry
{
public DateTime Timestamp { get; set; }
public AuctionLogLevel Level { get; set; }
public AuctionLogCategory Category { get; set; }
public string Message { get; set; } = "";
/// <summary>
/// Contatore deduplicazione (se > 1, il messaggio è stato ripetuto)
/// </summary>
public int RepeatCount { get; set; } = 1;
/// <summary>
/// Formato compatto per display: solo ora con millisecondi
/// </summary>
public string TimeDisplay => Timestamp.ToString("HH:mm:ss.fff");
/// <summary>
/// Icona Bootstrap per il livello
/// </summary>
public string LevelIcon => Level switch
{
AuctionLogLevel.Error => "bi-x-circle-fill",
AuctionLogLevel.Warning => "bi-exclamation-triangle-fill",
AuctionLogLevel.Success => "bi-check-circle-fill",
AuctionLogLevel.Bid => "bi-hand-index-thumb-fill",
AuctionLogLevel.Strategy => "bi-shield-fill",
AuctionLogLevel.Timing => "bi-stopwatch-fill",
AuctionLogLevel.Debug => "bi-bug-fill",
_ => "bi-info-circle-fill"
};
/// <summary>
/// Classe CSS per il livello
/// </summary>
public string LevelClass => Level switch
{
AuctionLogLevel.Error => "alog-error",
AuctionLogLevel.Warning => "alog-warning",
AuctionLogLevel.Success => "alog-success",
AuctionLogLevel.Bid => "alog-bid",
AuctionLogLevel.Strategy => "alog-strategy",
AuctionLogLevel.Timing => "alog-timing",
AuctionLogLevel.Debug => "alog-debug",
_ => "alog-info"
};
/// <summary>
/// Label breve del livello
/// </summary>
public string LevelLabel => Level switch
{
AuctionLogLevel.Error => "ERR",
AuctionLogLevel.Warning => "WARN",
AuctionLogLevel.Success => "OK",
AuctionLogLevel.Bid => "BID",
AuctionLogLevel.Strategy => "STRAT",
AuctionLogLevel.Timing => "TIME",
AuctionLogLevel.Debug => "DBG",
_ => "INFO"
};
/// <summary>
/// Label della categoria
/// </summary>
public string CategoryLabel => Category switch
{
AuctionLogCategory.Ticker => "Ticker",
AuctionLogCategory.Price => "Prezzo",
AuctionLogCategory.Reset => "Reset",
AuctionLogCategory.BidAttempt => "Puntata",
AuctionLogCategory.BidResult => "Risultato",
AuctionLogCategory.Strategy => "Strategia",
AuctionLogCategory.Value => "Valore",
AuctionLogCategory.Competition => "Compet.",
AuctionLogCategory.Limit => "Limite",
AuctionLogCategory.Diagnostic => "Diagn.",
AuctionLogCategory.Status => "Stato",
AuctionLogCategory.Polling => "Poll",
_ => "Generale"
};
}
/// <summary>
/// Livello di gravità del log per-asta
/// </summary>
public enum AuctionLogLevel
{
Debug = 0,
Info = 1,
Timing = 2,
Strategy = 3,
Bid = 4,
Success = 5,
Warning = 6,
Error = 7
}
/// <summary>
/// Categoria del log per filtraggio e raggruppamento
/// </summary>
public enum AuctionLogCategory
{
General,
Ticker,
Price,
Reset,
BidAttempt,
BidResult,
Strategy,
Value,
Competition,
Limit,
Diagnostic,
Status,
Polling
}
}

View File

@@ -17,6 +17,7 @@ namespace AutoBidder.Models
public double AvgFinalPrice { get; set; }
public double? MinFinalPrice { get; set; }
public double? MaxFinalPrice { get; set; }
public double? MedianFinalPrice { get; set; }
// Statistiche puntate
public double AvgBidsToWin { get; set; }
@@ -43,6 +44,11 @@ namespace AutoBidder.Models
public int? UserDefaultMaxBids { get; set; }
public int? UserDefaultBidBeforeDeadlineMs { get; set; }
/// <summary>
/// Se true, usa i limiti personalizzati del prodotto. Se false, usa i globali.
/// </summary>
public bool UseCustomLimits { get; set; }
// JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; }

View File

@@ -457,20 +457,41 @@
<!-- TAB LOG -->
<div class="tab-pane fade" id="content-log" role="tabpanel">
<div class="tab-panel-content">
<div class="log-box-compact">
@if (selectedAuction.AuctionLog.Any())
{
@foreach (var logEntry in GetAuctionLog(selectedAuction))
{
<div class="log-entry">@logEntry</div>
}
}
else
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log disponibile.</div>
}
</div>
<div class="tab-panel-content p-0">
@if (selectedAuction.AuctionLog.Any())
{
<div class="auction-log-grid">
<div class="alog-header">
<span class="alog-col-time">Ora</span>
<span class="alog-col-level">Livello</span>
<span class="alog-col-cat">Categoria</span>
<span class="alog-col-msg">Messaggio</span>
</div>
<div class="alog-body">
@foreach (var entry in GetAuctionLog(selectedAuction))
{
<div class="alog-row @entry.LevelClass">
<span class="alog-col-time">@entry.TimeDisplay</span>
<span class="alog-col-level">
<i class="bi @entry.LevelIcon"></i> @entry.LevelLabel
</span>
<span class="alog-col-cat">@entry.CategoryLabel</span>
<span class="alog-col-msg">
@entry.Message
@if (entry.RepeatCount > 1)
{
<span class="alog-repeat">x@entry.RepeatCount</span>
}
</span>
</div>
}
</div>
</div>
}
else
{
<div class="text-muted p-3"><i class="bi bi-inbox"></i> Nessun log disponibile.</div>
}
</div>
</div>
</div>

View File

@@ -102,7 +102,6 @@ namespace AutoBidder.Pages
private string? sessionUsername;
private int sessionRemainingBids;
private double sessionShopCredit;
private int sessionAuctionsWon;
// Recommended limits
private bool isLoadingRecommendations = false;
@@ -527,18 +526,56 @@ namespace AutoBidder.Pages
var productName = ExtractProductNameFromUrl(addDialogUrl);
var tempName = string.IsNullOrWhiteSpace(productName) ? $"Asta {auctionId}" : productName;
// Carica limiti dal database prodotti se disponibili
double minPrice = settings.DefaultMinPrice;
double maxPrice = settings.DefaultMaxPrice;
int maxClicks = settings.DefaultMaxClicks;
int bidDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
// Se abilitato, cerca limiti salvati per questo prodotto
// Nota: il nome estratto dall'URL è approssimativo. I limiti verranno
// ri-applicati in FetchAuctionDetailsInBackgroundAsync con il nome REALE.
if (!string.IsNullOrWhiteSpace(productName) &&
settings.NewAuctionLimitsPriority == "ProductStats" &&
StatsService.IsAvailable)
{
try
{
var productKey = ProductStatisticsService.GenerateProductKey(productName);
var productStats = StatsService.GetProductStats(productKey);
if (productStats != null && productStats.UseCustomLimits)
{
// Usa limiti personalizzati se abilitati per questo prodotto
if (productStats.UserDefaultMinPrice.HasValue)
minPrice = productStats.UserDefaultMinPrice.Value;
if (productStats.UserDefaultMaxPrice.HasValue)
maxPrice = productStats.UserDefaultMaxPrice.Value;
if (productStats.UserDefaultMaxBids.HasValue)
maxClicks = productStats.UserDefaultMaxBids.Value;
if (productStats.UserDefaultBidBeforeDeadlineMs.HasValue)
bidDeadlineMs = productStats.UserDefaultBidBeforeDeadlineMs.Value;
AddLog($"[STATS] Limiti prodotto (da URL): €{minPrice:F2}-€{maxPrice:F2}, MaxClicks={maxClicks}");
}
}
catch (Exception ex)
{
AddLog($"[STATS WARN] Impossibile caricare limiti prodotto: {ex.Message}");
}
}
// Crea nuova asta
var newAuction = new AuctionInfo
{
AuctionId = auctionId,
Name = tempName,
OriginalUrl = addDialogUrl,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
BidBeforeDeadlineMs = bidDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
MinPrice = minPrice,
MaxPrice = maxPrice,
MaxClicks = maxClicks,
IsActive = isActive,
IsPaused = isPaused
};
@@ -640,7 +677,60 @@ namespace AutoBidder.Pages
AddLog($"[FETCH] Info prodotto: Valore={auction.BuyNowPrice:F2}€, Spedizione={auction.ShippingCost:F2}€");
}
// 3. Salva se qualcosa è cambiato
// 3. Cerca limiti prodotto dal database con il nome REALE
if (!string.IsNullOrWhiteSpace(auction.Name))
{
var settings = AutoBidder.Utilities.SettingsManager.Load();
if (settings.NewAuctionLimitsPriority == "ProductStats" && StatsService.IsAvailable)
{
try
{
var productKey = ProductStatisticsService.GenerateProductKey(auction.Name);
var productStats = StatsService.GetProductStats(productKey);
if (productStats != null && productStats.UseCustomLimits)
{
bool limitsApplied = false;
if (productStats.UserDefaultMinPrice.HasValue)
{
auction.MinPrice = productStats.UserDefaultMinPrice.Value;
limitsApplied = true;
}
if (productStats.UserDefaultMaxPrice.HasValue)
{
auction.MaxPrice = productStats.UserDefaultMaxPrice.Value;
limitsApplied = true;
}
if (productStats.UserDefaultMaxBids.HasValue)
{
auction.MaxClicks = productStats.UserDefaultMaxBids.Value;
limitsApplied = true;
}
if (productStats.UserDefaultBidBeforeDeadlineMs.HasValue)
{
auction.BidBeforeDeadlineMs = productStats.UserDefaultBidBeforeDeadlineMs.Value;
limitsApplied = true;
}
if (limitsApplied)
{
AddLog($"[STATS] Limiti prodotto applicati dal DB: €{auction.MinPrice:F2}-€{auction.MaxPrice:F2}, Anticipo={auction.BidBeforeDeadlineMs}ms (key={productKey})");
}
}
else
{
AddLog($"[STATS] Nessun limite trovato per productKey={productKey}");
}
}
catch (Exception ex)
{
AddLog($"[STATS WARN] Errore ricerca limiti prodotto: {ex.Message}");
}
}
}
// 4. Salva se qualcosa è cambiato
if (updated)
{
SaveAuctions();
@@ -1212,32 +1302,44 @@ namespace AutoBidder.Pages
private IEnumerable<AuctionInfo> GetSortedAuctions()
{
var list = auctions.AsEnumerable();
list = auctionSortColumn switch
try
{
"stato" => auctionSortAscending
? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused)
: list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused),
"nome" => auctionSortAscending
? list.OrderBy(a => a.Name)
: list.OrderByDescending(a => a.Name),
"prezzo" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Price ?? 0)
: list.OrderByDescending(a => a.LastState?.Price ?? 0),
"timer" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Timer ?? 999)
: list.OrderByDescending(a => a.LastState?.Timer ?? 999),
"puntate" => auctionSortAscending
? list.OrderBy(a => a.BidsUsedOnThisAuction ?? 0)
: list.OrderByDescending(a => a.BidsUsedOnThisAuction ?? 0),
"ping" => auctionSortAscending
? list.OrderBy(a => a.PollingLatencyMs)
: list.OrderByDescending(a => a.PollingLatencyMs),
_ => list
};
// Protezione null-safety
if (auctions == null || auctions.Count == 0)
return Enumerable.Empty<AuctionInfo>();
return list;
var list = auctions.AsEnumerable();
list = auctionSortColumn switch
{
"stato" => auctionSortAscending
? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused)
: list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused),
"nome" => auctionSortAscending
? list.OrderBy(a => a.Name)
: list.OrderByDescending(a => a.Name),
"prezzo" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Price ?? 0)
: list.OrderByDescending(a => a.LastState?.Price ?? 0),
"timer" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Timer ?? 999)
: list.OrderByDescending(a => a.LastState?.Timer ?? 999),
"puntate" => auctionSortAscending
? list.OrderBy(a => a.BidsUsedOnThisAuction ?? 0)
: list.OrderByDescending(a => a.BidsUsedOnThisAuction ?? 0),
"ping" => auctionSortAscending
? list.OrderBy(a => a.PollingLatencyMs)
: list.OrderByDescending(a => a.PollingLatencyMs),
_ => list
};
return list;
}
catch
{
// Fallback sicuro in caso di errore
return Enumerable.Empty<AuctionInfo>();
}
}
// ?? NUOVI METODI: Visualizzazione valori prodotto
@@ -1375,9 +1477,9 @@ namespace AutoBidder.Pages
}
}
private IEnumerable<string> GetAuctionLog(AuctionInfo auction)
private IEnumerable<AuctionLogEntry> GetAuctionLog(AuctionInfo auction)
{
return auction.AuctionLog.TakeLast(50);
return auction.AuctionLog.TakeLast(100);
}
/// <summary>
@@ -1444,7 +1546,6 @@ namespace AutoBidder.Pages
sessionUsername = savedSession.Username;
sessionRemainingBids = savedSession.RemainingBids;
sessionShopCredit = savedSession.ShopCredit;
sessionAuctionsWon = 0; // TODO: add to BidooSession model
// Inizializza AuctionMonitor con la sessione salvata
if (!string.IsNullOrEmpty(savedSession.CookieString))
@@ -1458,7 +1559,6 @@ namespace AutoBidder.Pages
sessionUsername = session?.Username;
sessionRemainingBids = session?.RemainingBids ?? 0;
sessionShopCredit = session?.ShopCredit ?? 0;
sessionAuctionsWon = 0;
}
}
@@ -1475,7 +1575,6 @@ namespace AutoBidder.Pages
sessionUsername = session.Username;
sessionRemainingBids = session.RemainingBids;
sessionShopCredit = session.ShopCredit;
sessionAuctionsWon = 0; // TODO: add to BidooSession model
// Salva sessione aggiornata
AutoBidder.Services.SessionManager.SaveSession(session);
@@ -1541,8 +1640,6 @@ namespace AutoBidder.Pages
// Applica comunque ma con avviso
selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions();
@@ -1554,8 +1651,6 @@ namespace AutoBidder.Pages
// Applica limiti con buona confidenza
selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions();

View File

@@ -206,6 +206,7 @@
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
<div class="form-text">0 = punta a qualsiasi prezzo. Il prezzo deve essere ? a questo valore per puntare.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label>
@@ -217,27 +218,16 @@
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
<div class="form-text">0 = nessun limite. Se il prezzo supera questo valore, SMETTE di puntare.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset massimi</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
<div class="col-12">
<label class="form-label fw-bold"><i class="bi bi-database-gear"></i> Priorità limiti nuove aste</label>
<select class="form-select" @bind="settings.NewAuctionLimitsPriority">
<option value="ProductStats">Usa limiti salvati nelle statistiche prodotto</option>
<option value="GlobalDefaults">Usa sempre limiti globali</option>
</select>
<div class="form-text">
Se "Statistiche prodotto", quando aggiungi un'asta di un prodotto già salvato, verranno usati i limiti personalizzati delle statistiche invece di quelli globali.
</div>
</div>
<div class="col-12 col-md-6">
@@ -854,8 +844,6 @@ private System.Threading.Timer? updateTimer;
auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
auction.MinPrice = settings.DefaultMinPrice;
auction.MaxPrice = settings.DefaultMaxPrice;
auction.MinResets = settings.DefaultMinResets;
auction.MaxResets = settings.DefaultMaxResets;
// Resetta override per usare impostazioni globali
auction.AdvancedStrategiesEnabled = null;
@@ -925,14 +913,6 @@ private System.Threading.Timer? updateTimer;
auction.MaxPrice = settings.DefaultMaxPrice;
settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})";
break;
case nameof(settings.DefaultMinResets):
auction.MinResets = settings.DefaultMinResets;
settingLabel = $"Reset minimi ({settings.DefaultMinResets})";
break;
case nameof(settings.DefaultMaxResets):
auction.MaxResets = settings.DefaultMaxResets;
settingLabel = $"Reset massimi ({settings.DefaultMaxResets})";
break;
}
count++;
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,9 @@ namespace AutoBidder.Services
public event Action<string>? OnLog;
public event Action<string>? OnResetCountChanged;
// Throttling per log di blocco (evita spam nel log globale)
private readonly Dictionary<string, DateTime> _lastBlockLogTime = new();
/// <summary>
/// Evento fired quando un'asta termina (vinta, persa o chiusa).
/// Parametri: AuctionInfo, AuctionState finale, bool won (true se vinta dall'utente)
@@ -185,10 +188,8 @@ namespace AutoBidder.Services
auction.MinPrice = minPrice;
auction.MaxPrice = maxPrice;
auction.MinResets = minResets;
auction.MaxResets = maxResets;
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}: MinPrice={minPrice:F2}, MaxPrice={maxPrice:F2}, MinResets={minResets}, MaxResets={maxResets}");
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}: MinPrice={minPrice:F2}, MaxPrice={maxPrice:F2}");
return true;
}
}
@@ -210,8 +211,6 @@ namespace AutoBidder.Services
{
auction.MinPrice = minPrice;
auction.MaxPrice = maxPrice;
auction.MinResets = minResets;
auction.MaxResets = maxResets;
count++;
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}");
@@ -255,6 +254,7 @@ namespace AutoBidder.Services
int tickerIntervalMs = Math.Max(100, settings.TickerIntervalMs); // Minimo 100ms
int pollingIntervalMs = 500; // Poll API ogni 500ms max
DateTime lastPoll = DateTime.MinValue;
DateTime lastDiagnostic = DateTime.MinValue;
OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms");
@@ -279,42 +279,128 @@ namespace AutoBidder.Services
continue;
}
// === FASE 2: Poll API solo ogni pollingIntervalMs ===
var now = DateTime.UtcNow;
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
// Poll più frequente se vicino alla scadenza
bool anyNearDeadline = activeAuctions.Any(a =>
GetEstimatedTimerMs(a) < settings.StrategyCheckThresholdMs);
if (shouldPoll || anyNearDeadline)
// === DIAGNOSTICA PERIODICA (ogni 30s) ===
var nowDiag = DateTime.UtcNow;
if ((nowDiag - lastDiagnostic).TotalSeconds >= 30)
{
var pollTasks = activeAuctions.Select(a => PollAuctionState(a, token));
await Task.WhenAll(pollTasks);
lastPoll = now;
lastDiagnostic = nowDiag;
settings = SettingsManager.Load(); // Ricarica impostazioni
foreach (var a in activeAuctions.Where(x => !x.IsPaused))
{
var timer = a.LastState?.Timer ?? 0;
var price = a.LastState?.Price ?? 0;
int offset = a.BidBeforeDeadlineMs > 0 ? a.BidBeforeDeadlineMs : settings.DefaultBidBeforeDeadlineMs;
double estimatedMs = GetEstimatedTimerMs(a);
var statusParts = new List<string>();
statusParts.Add($"Timer={timer:F1}s");
statusParts.Add($"Stima={estimatedMs:F0}ms");
statusParts.Add($"€{price:F2}");
statusParts.Add($"Offset={offset}ms");
statusParts.Add($"RawMs={a.LastRawTimer:F0}");
// Indica perché potrebbe non puntare
if (a.MaxPrice > 0 && price > a.MaxPrice)
statusParts.Add($"⛔MaxPrice={a.MaxPrice:F2}");
if (a.MinPrice > 0 && price < a.MinPrice)
statusParts.Add($"⛔MinPrice={a.MinPrice:F2}");
a.AddLog($"[DIAG] {string.Join(" | ", statusParts)}");
}
}
// === FASE 3: TICKER CHECK - Verifica timing per ogni asta ===
// === FASE 2: PRE-BID CHECK (zero I/O, solo timer locale) ===
// CRITICO: Controlla il ticker PRIMA del poll per le aste nella zona di puntata.
// Il poll prende 40-100ms di rete. Con più aste near-deadline,
// Task.WhenAll aspetta il poll più lento → il tick effettivo è 100-200ms.
// Se l'offset è 200ms, la finestra di puntata è PIÙ STRETTA del tick:
// Tick N: estimated=300ms > offset → non trigghera
// [poll prende 150ms]
// Tick N+1: estimated=-50ms → fuori finestra → MANCATA!
// Il pre-bid check usa lo stato dal poll PRECEDENTE (< 100ms fa),
// che è sufficientemente fresco per IsMyBid, prezzo e ultimo puntatore.
foreach (var auction in activeAuctions)
{
if (auction.IsPaused || auction.LastState == null) continue;
// Calcola timer stimato LOCALMENTE (più preciso del polling)
double estimatedTimerMs = GetEstimatedTimerMs(auction);
// Offset configurato dall'utente (SENZA compensazioni)
int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
// TRIGGER: Timer <= Offset configurato dall'utente
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > 0)
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > -tickerIntervalMs)
{
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, token);
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, settings, token);
}
}
// === FASE 4: Delay fisso del ticker ===
// === FASE 3: Poll API ===
// Il poll aggiorna dati (prezzo, ultimo puntatore, timer) per il prossimo tick.
// Se rileva la fine dell'asta, la salva in PendingEndState (non la processa subito).
// Le aste nella zona critica (< offset*2 ms) NON vengono pollate:
// il poll prenderebbe tempo prezioso e i dati locali sono sufficienti.
var now = DateTime.UtcNow;
bool shouldPollAll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
if (shouldPollAll)
{
// Poll normale: tutte le aste attive
var pollTasks = activeAuctions.Select(a => PollAuctionState(a, token));
await Task.WhenAll(pollTasks);
lastPoll = now;
}
else
{
// Poll rapido: SOLO le aste vicine alla scadenza MA non nella zona critica
// Se il timer è < offset*2, il poll ruberebbe tempo al prossimo pre-bid check
var nearDeadlineAuctions = activeAuctions.Where(a =>
{
double est = GetEstimatedTimerMs(a);
int off = a.BidBeforeDeadlineMs > 0
? a.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
return est < settings.StrategyCheckThresholdMs && est > off * 2;
}).ToList();
if (nearDeadlineAuctions.Count > 0)
{
var pollTasks = nearDeadlineAuctions.Select(a => PollAuctionState(a, token));
await Task.WhenAll(pollTasks);
}
}
// === FASE 4: POST-POLL TICKER CHECK ===
// Per aste che sono entrate nella zona di puntata DURANTE il poll.
// Le aste già puntate dal pre-bid check vengono skippate da BidScheduled.
foreach (var auction in activeAuctions)
{
if (auction.IsPaused || auction.LastState == null) continue;
double estimatedTimerMs = GetEstimatedTimerMs(auction);
int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > -tickerIntervalMs)
{
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, settings, token);
}
}
// === FASE 4: Processa aste terminate (deferred) ===
// Solo ORA gestiamo le aste che il poll ha marcato come terminate.
// Il ticker ha avuto la sua ultima occasione nella Fase 3.
foreach (var auction in activeAuctions)
{
if (auction.PendingEndState != null)
{
HandleAuctionEnded(auction, auction.PendingEndState);
auction.PendingEndState = null;
}
}
// === FASE 5: Delay fisso del ticker ===
await Task.Delay(tickerIntervalMs, token);
}
catch (OperationCanceledException)
@@ -345,9 +431,11 @@ namespace AutoBidder.Services
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
// Timer stimato = timer raw - tempo trascorso
// NON clampare a 0: il ticker usa valori leggermente negativi
// per catturare la finestra quando il timer scade tra due tick
double estimated = auction.LastRawTimer - elapsed;
return Math.Max(0, estimated);
return estimated;
}
/// <summary>
@@ -388,12 +476,19 @@ namespace AutoBidder.Services
EnsureCurrentBidInHistory(auction, state);
}
// Gestione fine asta
// Gestione fine asta — DIFFERITA
// Non chiamare HandleAuctionEnded qui: il ticker deve avere
// un'ultima occasione di puntare con i dati freschi del poll.
// Lo stato di fine viene salvato in PendingEndState e processato
// dal loop principale DOPO il ticker check.
if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost ||
state.Status == AuctionStatus.Closed)
{
HandleAuctionEnded(auction, state);
// Aggiorna LastState con i dati più freschi (ultimo puntatore, prezzo)
// così il ticker può fare controlli accurati (es. IsMyBid, prezzo)
auction.LastState = state;
auction.PendingEndState = state;
return;
}
@@ -405,6 +500,15 @@ namespace AutoBidder.Services
auction.LastState = state;
// ?? FIX CRITICO: Aggiorna timer locale per interpolazione tra poll
// Senza questo, GetEstimatedTimerMs restituisce sempre il valore
// statico dell'ultimo poll e il countdown non funziona
if (state.Timer > 0)
{
auction.LastRawTimer = state.Timer * 1000; // Converti secondi → millisecondi
auction.LastDeadlineUpdateUtc = DateTime.UtcNow;
}
if (shouldNotify)
{
OnAuctionUpdated?.Invoke(state);
@@ -538,31 +642,64 @@ namespace AutoBidder.Services
return false;
}
/// <summary>
/// Logga un blocco nel log globale con throttling per evitare spam.
/// Ogni chiave (auctionId+reason) può loggare al massimo una volta ogni 10 secondi.
/// </summary>
private void LogBlockThrottled(AuctionInfo auction, string reason, string message)
{
var key = $"{auction.AuctionId}_{reason}";
var now = DateTime.UtcNow;
lock (_lastBlockLogTime)
{
if (_lastBlockLogTime.TryGetValue(key, out var lastTime))
{
if ((now - lastTime).TotalSeconds < 10)
return; // Throttle: già loggato di recente
}
_lastBlockLogTime[key] = now;
// Pulizia periodica entries vecchie (max 100)
if (_lastBlockLogTime.Count > 100)
{
var oldKeys = _lastBlockLogTime
.Where(kv => (now - kv.Value).TotalMinutes > 5)
.Select(kv => kv.Key)
.ToList();
foreach (var k in oldKeys)
_lastBlockLogTime.Remove(k);
}
}
OnLog?.Invoke(message);
}
/// <summary>
/// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato.
/// NESSUNA compensazione automatica della latenza - l'utente decide il timing esatto.
/// </summary>
private async Task TryPlaceBidTicker(AuctionInfo auction, double estimatedTimerMs, int offsetMs, CancellationToken token)
private async Task TryPlaceBidTicker(AuctionInfo auction, double estimatedTimerMs, int offsetMs, AppSettings settings, CancellationToken token)
{
var settings = SettingsManager.Load();
var state = auction.LastState;
if (state == null || state.IsMyBid || estimatedTimerMs <= 0) return;
// Log timing se abilitato
if (settings.LogTiming)
{
auction.AddLog($"[TICKER] Timer stimato={estimatedTimerMs:F0}ms <= Offset={offsetMs}ms");
}
// Log timing dettagliato per ogni check del ticker
auction.AddLog($"Timer={estimatedTimerMs:F0}ms | Offset={offsetMs}ms | Prezzo=€{state.Price:F2} | Ultimo={state.LastBidder ?? "-"}",
Models.AuctionLogLevel.Timing, Models.AuctionLogCategory.Ticker);
// === PROTEZIONE DOPPIA PUNTATA ===
// Reset se timer è aumentato (qualcuno ha puntato = nuovo ciclo)
if (estimatedTimerMs > auction.LastScheduledTimerMs + 500)
{
if (auction.BidScheduled)
{
auction.AddLog($"Reset ciclo: timer salito {auction.LastScheduledTimerMs:F0}→{estimatedTimerMs:F0}ms (qualcuno ha puntato)",
Models.AuctionLogLevel.Timing, Models.AuctionLogCategory.Ticker);
}
auction.BidScheduled = false;
}
// Reset se passato troppo tempo dall'ultima puntata
if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
{
@@ -572,6 +709,8 @@ namespace AutoBidder.Services
// Skip se già schedulata per questo ciclo
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs) < 200)
{
auction.AddLog($"Skip: già schedulata per questo ciclo (Δ={Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs):F0}ms)",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker);
return;
}
@@ -579,18 +718,23 @@ namespace AutoBidder.Services
if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
{
var cooldownRemaining = 1000 - (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds;
auction.AddLog($"Cooldown attivo: {cooldownRemaining:F0}ms rimanenti",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker);
return;
}
// === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target ===
// Evita calcoli inutili quando siamo lontani
if (estimatedTimerMs > settings.StrategyCheckThresholdMs)
{
return;
}
// === CONTROLLI FONDAMENTALI ===
if (!ShouldBid(auction, state))
auction.AddLog($"→ Inizio controlli puntata (timer={estimatedTimerMs:F0}ms ≤ soglia={settings.StrategyCheckThresholdMs}ms)",
Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt);
if (!ShouldBid(auction, state, settings))
{
return;
}
@@ -601,17 +745,22 @@ namespace AutoBidder.Services
if (!decision.ShouldBid)
{
auction.AddLog($"[STRATEGY] {decision.Reason}");
auction.AddLog($"⛔ Strategia blocca: {decision.Reason}",
Models.AuctionLogLevel.Strategy, Models.AuctionLogCategory.Strategy);
LogBlockThrottled(auction, "STRATEGY", $"[STRATEGY] {auction.Name}: {decision.Reason}");
return;
}
auction.AddLog($"✓ Tutti i controlli superati → PUNTATA!",
Models.AuctionLogLevel.Bid, Models.AuctionLogCategory.BidAttempt);
// === ESEGUI PUNTATA ===
auction.BidScheduled = true;
auction.LastScheduledTimerMs = estimatedTimerMs;
auction.AddLog($"[BID] Timer locale={estimatedTimerMs:F0}ms, Offset utente={offsetMs}ms");
await ExecuteBid(auction, state, token);
await ExecuteBid(auction, state, settings, token);
}
// ═══════════════════════════════════════════════════════════════════
@@ -639,13 +788,13 @@ namespace AutoBidder.Services
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
double timerMs = state.Timer * 1000;
await TryPlaceBidTicker(auction, timerMs, offsetMs, token);
await TryPlaceBidTicker(auction, timerMs, offsetMs, settings, token);
}
/// <summary>
/// Esegue la puntata e registra metriche
/// </summary>
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, CancellationToken token)
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, AppSettings settings, CancellationToken token)
{
try
{
@@ -694,7 +843,6 @@ namespace AutoBidder.Services
else
{
var pollingPing = auction.PollingLatencyMs;
var settings = SettingsManager.Load();
// Rileva errore "timer scaduto" per feedback utente
bool isLateBid = result.Error?.Contains("timer") == true ||
@@ -740,16 +888,11 @@ namespace AutoBidder.Services
}
}
private bool ShouldBid(AuctionInfo auction, AuctionState state)
private bool ShouldBid(AuctionInfo auction, AuctionState state, AppSettings? settings = null)
{
var settings = Utilities.SettingsManager.Load();
settings ??= Utilities.SettingsManager.Load();
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] === INIZIO CONTROLLI PUNTATA ===");
}
// ?? CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili)
// CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili)
if (settings.ValueCheckEnabled &&
auction.BuyNowPrice.HasValue &&
auction.BuyNowPrice.Value > 0 &&
@@ -757,27 +900,26 @@ namespace AutoBidder.Services
auction.CalculatedValue.Savings.HasValue &&
!auction.CalculatedValue.IsWorthIt)
{
// Usa la percentuale configurabile dall'utente
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage)
{
// 🔥 Logga SEMPRE - è un blocco frequente e importante
auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto");
auction.AddLog($"⛔ Risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto",
Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Value);
LogBlockThrottled(auction, "VALUE", $"[VALUE] {auction.Name}: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% insufficiente");
return false;
}
}
if (settings.LogTiming && settings.ValueCheckEnabled)
else
{
auction.AddLog($"[DEBUG] ✓ Controllo convenienza OK");
auction.AddLog($"✓ Convenienza OK (check={settings.ValueCheckEnabled}, buyNow={auction.BuyNowPrice?.ToString("F2") ?? "N/D"})",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Value);
}
// ?? CONTROLLO ANTI-COLLISIONE (OPZIONALE): Rileva aste troppo "affollate"
// DISABILITATO DI DEFAULT - può far perdere aste competitive!
// CONTROLLO ANTI-COLLISIONE (OPZIONALE)
if (settings.HardcodedAntiCollisionEnabled)
{
var recentBidsThreshold = 10; // secondi
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
var recentBidsThreshold = 10;
var maxActiveBidders = 3;
try
{
@@ -791,125 +933,108 @@ namespace AutoBidder.Services
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
}
auction.AddLog($"Competizione: {activeBidders} bidder attivi (soglia={maxActiveBidders})",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Competition);
if (activeBidders >= maxActiveBidders)
{
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
var session = _apiClient.GetSession();
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
if (lastBid != null &&
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
{
auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP");
auction.AddLog($" Asta affollata: {activeBidders} bidder attivi",
Models.AuctionLogLevel.Strategy, Models.AuctionLogCategory.Competition);
return false;
}
}
}
catch { /* Ignora errori nel controllo competizione */ }
catch { }
}
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] ? Controllo competizione OK");
}
// ?? CONTROLLO 1: Limite minimo puntate residue
// CONTROLLO 1: Limite minimo puntate residue
if (settings.MinimumRemainingBids > 0)
{
var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{
// 🔥 Logga SEMPRE - è un blocco importante
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
auction.AddLog($"⛔ Puntate residue ({session.RemainingBids}) ≤ limite ({settings.MinimumRemainingBids})",
Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Limit);
LogBlockThrottled(auction, "LIMIT", $"[LIMIT] {auction.Name}: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
return false;
}
if (settings.LogTiming && session != null)
else if (session != null)
{
auction.AddLog($"[DEBUG] ✓ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
auction.AddLog($"✓ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit);
}
}
// ? 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");
}
auction.AddLog($"✓ Sono già vincitore corrente - skip",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.BidAttempt);
return false;
}
// ?? CONTROLLO 3: MinPrice/MaxPrice
// CONTROLLO 3: Limite puntate per questa asta
// Controlla MaxClicks (impostato dall'utente nella griglia o da product stats)
{
int maxBids = auction.MaxClicks; // 0 = illimitato
int usedBids = auction.BidsUsedOnThisAuction ?? 0;
if (maxBids > 0 && usedBids >= maxBids)
{
auction.AddLog($"⛔ Limite puntate raggiunto ({usedBids}/{maxBids})",
Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Limit);
LogBlockThrottled(auction, "MAXBIDS", $"[LIMIT] {auction.Name}: puntate {usedBids}/{maxBids} - STOP");
return false;
}
if (maxBids > 0)
{
auction.AddLog($"✓ Puntate OK ({usedBids}/{maxBids})",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit);
}
}
// CONTROLLO 4: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{
// 🔥 Logga SEMPRE questo blocco - è critico per capire perché non punta
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
auction.AddLog($"⛔ Prezzo €{state.Price:F2} < Min €{auction.MinPrice:F2}",
Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Price);
LogBlockThrottled(auction, "PRICE_LOW", $"[PRICE] {auction.Name}: €{state.Price:F2} < Min €{auction.MinPrice:F2} - NON PUNTA");
return false;
}
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{
// 🔥 Logga SEMPRE questo blocco - è critico
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
auction.AddLog($"⛔ Prezzo €{state.Price:F2} > Max €{auction.MaxPrice:F2}",
Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Price);
LogBlockThrottled(auction, "PRICE_HIGH", $"[PRICE] {auction.Name}: €{state.Price:F2} > Max €{auction.MaxPrice:F2} - NON PUNTA");
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)
{
// 🔥 Logga SEMPRE - è un motivo comune di aste perse
auction.AddLog($"[RESET] Reset troppo bassi: {auction.ResetCount} < Min {auction.MinResets}");
return false;
}
if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets)
{
// 🔥 Logga SEMPRE - è un motivo comune di aste perse
auction.AddLog($"[RESET] Reset massimi raggiunti: {auction.ResetCount} >= Max {auction.MaxResets}");
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}])");
}
}
auction.AddLog($"✓ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "")}])",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Price);
// ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate)
// CONTROLLO 6: Cooldown
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)");
}
auction.AddLog($"Cooldown: {timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms",
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker);
return false;
}
}
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] === TUTTI I CONTROLLI SUPERATI ===");
}
auction.AddLog($"✓ Tutti i controlli ShouldBid superati",
Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt);
return true;
}
@@ -968,7 +1093,7 @@ namespace AutoBidder.Services
{
Timestamp = DateTime.UtcNow,
EventType = BidEventType.Reset,
Bidder = state.LastBidder,
Bidder = state.LastBidder ?? "",
Price = state.Price,
Timer = state.Timer,
Notes = $"Puntata: EUR{state.Price:F2}"

View File

@@ -747,6 +747,24 @@ namespace AutoBidder.Services
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
}),
new Migration(16, "Add UseCustomLimits flag to ProductStatistics", async (conn) => {
var sql = @"
ALTER TABLE ProductStatistics ADD COLUMN UseCustomLimits INTEGER NOT NULL DEFAULT 0;
";
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
}),
new Migration(17, "Add MedianFinalPrice to ProductStatistics", async (conn) => {
var sql = @"
ALTER TABLE ProductStatistics ADD COLUMN MedianFinalPrice REAL;
";
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
})
};
@@ -1418,14 +1436,14 @@ namespace AutoBidder.Services
var sql = @"
INSERT INTO ProductStatistics
(ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice,
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated)
VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
@avgFinalPrice, @minFinalPrice, @maxFinalPrice,
@avgFinalPrice, @minFinalPrice, @maxFinalPrice, @medianFinalPrice,
@avgBidsToWin, @minBidsToWin, @maxBidsToWin,
@avgResets, @minResets, @maxResets,
@recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
@@ -1439,6 +1457,7 @@ namespace AutoBidder.Services
AvgFinalPrice = @avgFinalPrice,
MinFinalPrice = @minFinalPrice,
MaxFinalPrice = @maxFinalPrice,
MedianFinalPrice = @medianFinalPrice,
AvgBidsToWin = @avgBidsToWin,
MinBidsToWin = @minBidsToWin,
MaxBidsToWin = @maxBidsToWin,
@@ -1469,6 +1488,7 @@ namespace AutoBidder.Services
new SqliteParameter("@avgFinalPrice", stats.AvgFinalPrice),
new SqliteParameter("@minFinalPrice", (object?)stats.MinFinalPrice ?? DBNull.Value),
new SqliteParameter("@maxFinalPrice", (object?)stats.MaxFinalPrice ?? DBNull.Value),
new SqliteParameter("@medianFinalPrice", (object?)stats.MedianFinalPrice ?? DBNull.Value),
new SqliteParameter("@avgBidsToWin", stats.AvgBidsToWin),
new SqliteParameter("@minBidsToWin", (object?)stats.MinBidsToWin ?? DBNull.Value),
new SqliteParameter("@maxBidsToWin", (object?)stats.MaxBidsToWin ?? DBNull.Value),
@@ -1498,12 +1518,12 @@ namespace AutoBidder.Services
{
var sql = @"
SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice,
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
HourlyStatsJson, LastUpdated, UseCustomLimits
FROM ProductStatistics
WHERE ProductKey = @productKey;
";
@@ -1526,25 +1546,27 @@ namespace AutoBidder.Services
AvgFinalPrice = reader.GetDouble(5),
MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6),
MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
AvgBidsToWin = reader.GetDouble(8),
MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9),
MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
AvgResets = reader.GetDouble(11),
MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12),
MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14),
RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
LastUpdated = reader.GetString(26)
MedianFinalPrice = reader.IsDBNull(8) ? null : reader.GetDouble(8),
AvgBidsToWin = reader.GetDouble(9),
MinBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
MaxBidsToWin = reader.IsDBNull(11) ? null : reader.GetInt32(11),
AvgResets = reader.GetDouble(12),
MinResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
MaxResets = reader.IsDBNull(14) ? null : reader.GetInt32(14),
RecommendedMinPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
RecommendedMaxPrice = reader.IsDBNull(16) ? null : reader.GetDouble(16),
RecommendedMinResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxResets = reader.IsDBNull(18) ? null : reader.GetInt32(18),
RecommendedMaxBids = reader.IsDBNull(19) ? null : reader.GetInt32(19),
UserDefaultMinPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMaxPrice = reader.IsDBNull(21) ? null : reader.GetDouble(21),
UserDefaultMinResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxResets = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultMaxBids = reader.IsDBNull(24) ? null : reader.GetInt32(24),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(25) ? null : reader.GetInt32(25),
HourlyStatsJson = reader.IsDBNull(26) ? null : reader.GetString(26),
LastUpdated = reader.GetString(27),
UseCustomLimits = !reader.IsDBNull(28) && reader.GetInt32(28) != 0
};
}
@@ -1590,12 +1612,12 @@ namespace AutoBidder.Services
{
var sql = @"
SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice,
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
HourlyStatsJson, LastUpdated, UseCustomLimits
FROM ProductStatistics
ORDER BY TotalAuctions DESC;
";
@@ -1619,25 +1641,27 @@ namespace AutoBidder.Services
AvgFinalPrice = reader.GetDouble(5),
MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6),
MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
AvgBidsToWin = reader.GetDouble(8),
MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9),
MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
AvgResets = reader.GetDouble(11),
MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12),
MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14),
RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
LastUpdated = reader.GetString(26)
MedianFinalPrice = reader.IsDBNull(8) ? null : reader.GetDouble(8),
AvgBidsToWin = reader.GetDouble(9),
MinBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
MaxBidsToWin = reader.IsDBNull(11) ? null : reader.GetInt32(11),
AvgResets = reader.GetDouble(12),
MinResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
MaxResets = reader.IsDBNull(14) ? null : reader.GetInt32(14),
RecommendedMinPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
RecommendedMaxPrice = reader.IsDBNull(16) ? null : reader.GetDouble(16),
RecommendedMinResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxResets = reader.IsDBNull(18) ? null : reader.GetInt32(18),
RecommendedMaxBids = reader.IsDBNull(19) ? null : reader.GetInt32(19),
UserDefaultMinPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMaxPrice = reader.IsDBNull(21) ? null : reader.GetDouble(21),
UserDefaultMinResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxResets = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultMaxBids = reader.IsDBNull(24) ? null : reader.GetInt32(24),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(25) ? null : reader.GetInt32(25),
HourlyStatsJson = reader.IsDBNull(26) ? null : reader.GetString(26),
LastUpdated = reader.GetString(27),
UseCustomLimits = !reader.IsDBNull(28) && reader.GetInt32(28) != 0
});
}
@@ -1665,7 +1689,8 @@ namespace AutoBidder.Services
public async Task UpdateProductUserDefaultsAsync(string productKey,
double? minPrice, double? maxPrice,
int? minResets, int? maxResets,
int? maxBids, int? bidBeforeDeadlineMs)
int? maxBids, int? bidBeforeDeadlineMs,
bool? useCustomLimits = null)
{
var sql = @"
UPDATE ProductStatistics
@@ -1675,6 +1700,7 @@ namespace AutoBidder.Services
UserDefaultMaxResets = @maxResets,
UserDefaultMaxBids = @maxBids,
UserDefaultBidBeforeDeadlineMs = @bidDeadline,
UseCustomLimits = CASE WHEN @useCustomLimits IS NOT NULL THEN @useCustomLimits ELSE UseCustomLimits END,
LastUpdated = @lastUpdated
WHERE ProductKey = @productKey;
";
@@ -1687,6 +1713,7 @@ namespace AutoBidder.Services
new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value),
new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value),
new SqliteParameter("@bidDeadline", (object?)bidBeforeDeadlineMs ?? DBNull.Value),
new SqliteParameter("@useCustomLimits", useCustomLimits.HasValue ? (object)(useCustomLimits.Value ? 1 : 0) : DBNull.Value),
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
);
}

View File

@@ -98,12 +98,14 @@ namespace AutoBidder.Services
stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice);
stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice);
stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice);
stats.MedianFinalPrice = CalculateMedian(wonResults.Select(r => r.FinalPrice).ToList());
}
else if (results.Any())
{
stats.AvgFinalPrice = results.Average(r => r.FinalPrice);
stats.MinFinalPrice = results.Min(r => r.FinalPrice);
stats.MaxFinalPrice = results.Max(r => r.FinalPrice);
stats.MedianFinalPrice = CalculateMedian(results.Select(r => r.FinalPrice).ToList());
}
// Statistiche puntate (usa WinnerBidsUsed se disponibile, altrimenti BidsUsed)
@@ -336,5 +338,15 @@ namespace AutoBidder.Services
double sumSquares = data.Sum(d => Math.Pow(d - avg, 2));
return Math.Sqrt(sumSquares / (data.Count - 1));
}
private static double CalculateMedian(List<double> data)
{
if (data.Count == 0) return 0;
var sorted = data.OrderBy(x => x).ToList();
int mid = sorted.Count / 2;
return sorted.Count % 2 == 0
? (sorted[mid - 1] + sorted[mid]) / 2.0
: sorted[mid];
}
}
}

View File

@@ -318,6 +318,25 @@ namespace AutoBidder.Services
return await _productStatsService.GetRecommendedLimitsAsync(productKey);
}
/// <summary>
/// Ottiene le statistiche di un singolo prodotto
/// </summary>
public ProductStatisticsRecord? GetProductStats(string productKey)
{
if (_productStatsService == null || !IsAvailable) return null;
try
{
// Carica statistiche dal database in modo sincrono
var allStats = _productStatsService.GetAllProductStatisticsAsync().GetAwaiter().GetResult();
return allStats.FirstOrDefault(p => p.ProductKey == productKey);
}
catch
{
return null;
}
}
/// <summary>
/// Ottiene tutte le statistiche prodotto
/// </summary>

View File

@@ -181,6 +181,14 @@ namespace AutoBidder.Utilities
/// </summary>
public bool AutoApplyProductDefaults { get; set; } = true;
/// <summary>
/// Scelta priorità limiti quando si aggiunge un'asta per un prodotto già salvato:
/// - "ProductStats": Usa i limiti personalizzati salvati nelle statistiche prodotto (UserDefaultMinPrice, ecc.)
/// - "GlobalDefaults": Usa sempre i limiti globali (DefaultMinPrice, DefaultMaxPrice, ecc.)
/// Default: "ProductStats" (consigliato per usare limiti specifici per prodotto)
/// </summary>
public string NewAuctionLimitsPriority { get; set; } = "ProductStats";
/// <summary>
/// Log stato asta (terminata, reset, ecc.) [STATUS]
/// Default: true
@@ -428,17 +436,40 @@ namespace AutoBidder.Utilities
private static readonly string _folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
private static readonly string _file = Path.Combine(_folder, "settings.json");
// Cache per evitare letture disco ripetute nel hot path (ticker loop 50ms)
private static readonly object _cacheLock = new();
private static AppSettings? _cached;
private static DateTime _cacheExpiry = DateTime.MinValue;
private const int CACHE_TTL_MS = 2000; // Ricarica da disco al massimo ogni 2s
public static AppSettings Load()
{
try
lock (_cacheLock)
{
if (!File.Exists(_file)) return new AppSettings();
var txt = File.ReadAllText(_file);
var s = JsonSerializer.Deserialize<AppSettings>(txt);
if (s == null) return new AppSettings();
return s;
var now = DateTime.UtcNow;
if (_cached != null && now < _cacheExpiry)
return _cached;
try
{
if (!File.Exists(_file))
{
_cached = new AppSettings();
}
else
{
var txt = File.ReadAllText(_file);
_cached = JsonSerializer.Deserialize<AppSettings>(txt) ?? new AppSettings();
}
}
catch
{
_cached ??= new AppSettings();
}
_cacheExpiry = now.AddMilliseconds(CACHE_TTL_MS);
return _cached;
}
catch { return new AppSettings(); }
}
public static void Save(AppSettings settings)
@@ -448,6 +479,13 @@ namespace AutoBidder.Utilities
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_file, txt);
// Invalida cache così il prossimo Load() legge i nuovi valori
lock (_cacheLock)
{
_cached = settings;
_cacheExpiry = DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS);
}
}
catch { }
}

View File

@@ -1303,3 +1303,181 @@
.btn-xs i {
font-size: 0.75rem;
}
/* ═══════════════════════════════════════════════════════════════════
LOG ASTA STRUTTURATO - GRIGLIA A COLONNE COMPATTA
═══════════════════════════════════════════════════════════════════ */
.auction-log-grid {
display: flex;
flex-direction: column;
height: 100%;
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
font-size: 0.72rem;
line-height: 1.2;
}
.alog-header {
display: flex;
gap: 0;
padding: 4px 6px;
background: rgba(255, 255, 255, 0.06);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-weight: 600;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(255, 255, 255, 0.5);
position: sticky;
top: 0;
z-index: 1;
}
.alog-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.alog-row {
display: flex;
gap: 0;
padding: 1px 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.025);
align-items: baseline;
transition: background 0.1s;
}
.alog-row:hover {
background: rgba(255, 255, 255, 0.04);
}
/* Colonne */
.alog-col-time {
flex: 0 0 85px;
color: rgba(255, 255, 255, 0.35);
font-variant-numeric: tabular-nums;
padding-right: 6px;
}
.alog-col-level {
flex: 0 0 62px;
font-weight: 600;
font-size: 0.65rem;
padding-right: 4px;
white-space: nowrap;
}
.alog-col-level i {
font-size: 0.6rem;
margin-right: 2px;
}
.alog-col-cat {
flex: 0 0 65px;
color: rgba(255, 255, 255, 0.4);
font-size: 0.65rem;
padding-right: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.alog-col-msg {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: rgba(255, 255, 255, 0.8);
}
/* Badge ripetizione */
.alog-repeat {
display: inline-block;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.5);
font-size: 0.6rem;
padding: 0 4px;
border-radius: 3px;
margin-left: 4px;
font-weight: 600;
}
/* === COLORI PER LIVELLO === */
/* Error */
.alog-error {
background: rgba(220, 53, 69, 0.08);
}
.alog-error .alog-col-level {
color: #ff4d5e;
}
.alog-error .alog-col-msg {
color: #ff7a85;
}
/* Warning */
.alog-warning {
background: rgba(255, 193, 7, 0.05);
}
.alog-warning .alog-col-level {
color: #ffc107;
}
.alog-warning .alog-col-msg {
color: rgba(255, 220, 130, 0.9);
}
/* Success */
.alog-success {
background: rgba(40, 167, 69, 0.08);
}
.alog-success .alog-col-level {
color: #4cff8e;
}
.alog-success .alog-col-msg {
color: #7dffb0;
}
/* Bid */
.alog-bid {
background: rgba(0, 123, 255, 0.08);
}
.alog-bid .alog-col-level {
color: #5eadff;
}
.alog-bid .alog-col-msg {
color: #8ec8ff;
}
/* Strategy */
.alog-strategy {
background: rgba(160, 80, 220, 0.08);
}
.alog-strategy .alog-col-level {
color: #c77dff;
}
.alog-strategy .alog-col-msg {
color: #dda0ff;
}
/* Timing */
.alog-timing .alog-col-level {
color: #17a2b8;
}
.alog-timing .alog-col-msg {
color: rgba(100, 200, 220, 0.85);
}
/* Debug */
.alog-debug {
opacity: 0.55;
}
.alog-debug .alog-col-level {
color: rgba(255, 255, 255, 0.3);
}
/* Info (default) */
.alog-info .alog-col-level {
color: rgba(255, 255, 255, 0.5);
}