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) public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
/// <summary> /// <summary>
/// [OBSOLETO] Numero massimo di puntate consentite - Non più utilizzato nell'UI /// Numero massimo di puntate consentite per questa asta (0 = illimitato).
/// Mantenuto per retrocompatibilità con salvataggi JSON esistenti /// Impostato dall'utente nella griglia statistiche o dai limiti prodotto.
/// Controllato in ShouldBid contro BidsUsedOnThisAuction.
/// </summary> /// </summary>
[Obsolete("MaxClicks non è più utilizzato. Usa invece la logica di limiti per prodotto.")]
[JsonPropertyName("MaxClicks")] [JsonPropertyName("MaxClicks")]
public int MaxClicks { get; set; } = 0; public int MaxClicks { get; set; } = 0;
@@ -107,6 +107,13 @@ namespace AutoBidder.Models
[JsonIgnore] [JsonIgnore]
public double LastScheduledTimerMs { get; set; } 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 // Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>(); public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
@@ -118,9 +125,9 @@ namespace AutoBidder.Models
[JsonPropertyName("RecentBids")] [JsonPropertyName("RecentBids")]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>(); 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] [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 // Flag runtime: indica che è in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore]
@@ -168,64 +175,141 @@ namespace AutoBidder.Models
/// <summary> /// <summary>
/// Aggiunge una voce al log dell'asta con deduplicazione e limite automatico di righe. /// Aggiunge una voce strutturata al log dell'asta con deduplicazione e limite righe.
/// Se il messaggio è identico all'ultimo, incrementa un contatore invece di duplicare. /// Parsifica automaticamente il tag [TAG] per determinare livello e categoria.
/// </summary> /// </summary>
/// <param name="message">Messaggio da aggiungere al log</param> public void AddLog(string message, int maxLines = 200)
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
public void AddLog(string message, int maxLines = 500)
{ {
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 var now = DateTime.Now;
#if DEBUG
System.Diagnostics.Debug.WriteLine($"[AddLog] {AuctionId}: {message}");
#endif
// ?? 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) if (AuctionLog.Count > 0)
{ {
var lastEntry = AuctionLog[^1]; // Ultimo elemento var last = AuctionLog[^1];
if (last.Message == cleanMessage && last.Category == category)
// Estrai il messaggio senza timestamp e contatore
var lastMessageStart = lastEntry.IndexOf(" - ");
if (lastMessageStart > 0)
{ {
var lastMessage = lastEntry.Substring(lastMessageStart + 3); last.RepeatCount++;
last.Timestamp = now;
// Rimuovi eventuale contatore esistente (es: " (x5)") return;
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;
}
} }
} }
// Nuovo messaggio diverso dall'ultimo AuctionLog.Add(new AuctionLogEntry
var entry = $"{timestamp} - {message}"; {
AuctionLog.Add(entry); Timestamp = now,
Level = level,
Category = category,
Message = cleanMessage
});
// Mantieni solo gli ultimi maxLines log
if (AuctionLog.Count > maxLines) if (AuctionLog.Count > maxLines)
{ {
int excessCount = AuctionLog.Count - maxLines; AuctionLog.RemoveRange(0, AuctionLog.Count - maxLines);
AuctionLog.RemoveRange(0, excessCount);
} }
} }
/// <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 public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms
// ??????????????????????????????????????????????????????????????? // ???????????????????????????????????????????????????????????????
@@ -428,6 +512,7 @@ namespace AutoBidder.Models
// Pulisci oggetti complessi // Pulisci oggetti complessi
LastState = null; LastState = null;
PendingEndState = null;
CalculatedValue = null; CalculatedValue = null;
DuelOpponent = null; DuelOpponent = null;
WinLimitDescription = 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 AvgFinalPrice { get; set; }
public double? MinFinalPrice { get; set; } public double? MinFinalPrice { get; set; }
public double? MaxFinalPrice { get; set; } public double? MaxFinalPrice { get; set; }
public double? MedianFinalPrice { get; set; }
// Statistiche puntate // Statistiche puntate
public double AvgBidsToWin { get; set; } public double AvgBidsToWin { get; set; }
@@ -43,6 +44,11 @@ namespace AutoBidder.Models
public int? UserDefaultMaxBids { get; set; } public int? UserDefaultMaxBids { get; set; }
public int? UserDefaultBidBeforeDeadlineMs { 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 // JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; } public string? HourlyStatsJson { get; set; }

View File

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

View File

@@ -102,7 +102,6 @@ namespace AutoBidder.Pages
private string? sessionUsername; private string? sessionUsername;
private int sessionRemainingBids; private int sessionRemainingBids;
private double sessionShopCredit; private double sessionShopCredit;
private int sessionAuctionsWon;
// Recommended limits // Recommended limits
private bool isLoadingRecommendations = false; private bool isLoadingRecommendations = false;
@@ -527,18 +526,56 @@ namespace AutoBidder.Pages
var productName = ExtractProductNameFromUrl(addDialogUrl); var productName = ExtractProductNameFromUrl(addDialogUrl);
var tempName = string.IsNullOrWhiteSpace(productName) ? $"Asta {auctionId}" : productName; 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 // Crea nuova asta
var newAuction = new AuctionInfo var newAuction = new AuctionInfo
{ {
AuctionId = auctionId, AuctionId = auctionId,
Name = tempName, Name = tempName,
OriginalUrl = addDialogUrl, OriginalUrl = addDialogUrl,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs, BidBeforeDeadlineMs = bidDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid, CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice, MinPrice = minPrice,
MaxPrice = settings.DefaultMaxPrice, MaxPrice = maxPrice,
MinResets = settings.DefaultMinResets, MaxClicks = maxClicks,
MaxResets = settings.DefaultMaxResets,
IsActive = isActive, IsActive = isActive,
IsPaused = isPaused IsPaused = isPaused
}; };
@@ -640,7 +677,60 @@ namespace AutoBidder.Pages
AddLog($"[FETCH] Info prodotto: Valore={auction.BuyNowPrice:F2}€, Spedizione={auction.ShippingCost:F2}€"); 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) if (updated)
{ {
SaveAuctions(); SaveAuctions();
@@ -1212,32 +1302,44 @@ namespace AutoBidder.Pages
private IEnumerable<AuctionInfo> GetSortedAuctions() private IEnumerable<AuctionInfo> GetSortedAuctions()
{ {
var list = auctions.AsEnumerable(); try
list = auctionSortColumn switch
{ {
"stato" => auctionSortAscending // Protezione null-safety
? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused) if (auctions == null || auctions.Count == 0)
: list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused), return Enumerable.Empty<AuctionInfo>();
"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; 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 // ?? 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> /// <summary>
@@ -1444,7 +1546,6 @@ namespace AutoBidder.Pages
sessionUsername = savedSession.Username; sessionUsername = savedSession.Username;
sessionRemainingBids = savedSession.RemainingBids; sessionRemainingBids = savedSession.RemainingBids;
sessionShopCredit = savedSession.ShopCredit; sessionShopCredit = savedSession.ShopCredit;
sessionAuctionsWon = 0; // TODO: add to BidooSession model
// Inizializza AuctionMonitor con la sessione salvata // Inizializza AuctionMonitor con la sessione salvata
if (!string.IsNullOrEmpty(savedSession.CookieString)) if (!string.IsNullOrEmpty(savedSession.CookieString))
@@ -1458,7 +1559,6 @@ namespace AutoBidder.Pages
sessionUsername = session?.Username; sessionUsername = session?.Username;
sessionRemainingBids = session?.RemainingBids ?? 0; sessionRemainingBids = session?.RemainingBids ?? 0;
sessionShopCredit = session?.ShopCredit ?? 0; sessionShopCredit = session?.ShopCredit ?? 0;
sessionAuctionsWon = 0;
} }
} }
@@ -1475,7 +1575,6 @@ namespace AutoBidder.Pages
sessionUsername = session.Username; sessionUsername = session.Username;
sessionRemainingBids = session.RemainingBids; sessionRemainingBids = session.RemainingBids;
sessionShopCredit = session.ShopCredit; sessionShopCredit = session.ShopCredit;
sessionAuctionsWon = 0; // TODO: add to BidooSession model
// Salva sessione aggiornata // Salva sessione aggiornata
AutoBidder.Services.SessionManager.SaveSession(session); AutoBidder.Services.SessionManager.SaveSession(session);
@@ -1541,8 +1640,6 @@ namespace AutoBidder.Pages
// Applica comunque ma con avviso // Applica comunque ma con avviso
selectedAuction.MinPrice = limits.MinPrice; selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice; selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions(); SaveAuctions();
@@ -1554,8 +1651,6 @@ namespace AutoBidder.Pages
// Applica limiti con buona confidenza // Applica limiti con buona confidenza
selectedAuction.MinPrice = limits.MinPrice; selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice; selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions(); SaveAuctions();

View File

@@ -206,6 +206,7 @@
<i class="bi bi-arrow-repeat"></i> <i class="bi bi-arrow-repeat"></i>
</button> </button>
</div> </div>
<div class="form-text">0 = punta a qualsiasi prezzo. Il prezzo deve essere ? a questo valore per puntare.</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label> <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> <i class="bi bi-arrow-repeat"></i>
</button> </button>
</div> </div>
<div class="form-text">0 = nessun limite. Se il prezzo supera questo valore, SMETTE di puntare.</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label> <label class="form-label fw-bold"><i class="bi bi-database-gear"></i> Priorità limiti nuove aste</label>
<div class="input-group"> <select class="form-select" @bind="settings.NewAuctionLimitsPriority">
<input type="number" class="form-control" @bind="settings.DefaultMinResets" /> <option value="ProductStats">Usa limiti salvati nelle statistiche prodotto</option>
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinResets))" <option value="GlobalDefaults">Usa sempre limiti globali</option>
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinResets))" </select>
title="Applica a tutte le aste"> <div class="form-text">
<i class="bi bi-arrow-repeat"></i> Se "Statistiche prodotto", quando aggiungi un'asta di un prodotto già salvato, verranno usati i limiti personalizzati delle statistiche invece di quelli globali.
</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> </div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@@ -854,8 +844,6 @@ private System.Threading.Timer? updateTimer;
auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs; auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
auction.MinPrice = settings.DefaultMinPrice; auction.MinPrice = settings.DefaultMinPrice;
auction.MaxPrice = settings.DefaultMaxPrice; auction.MaxPrice = settings.DefaultMaxPrice;
auction.MinResets = settings.DefaultMinResets;
auction.MaxResets = settings.DefaultMaxResets;
// Resetta override per usare impostazioni globali // Resetta override per usare impostazioni globali
auction.AdvancedStrategiesEnabled = null; auction.AdvancedStrategiesEnabled = null;
@@ -925,14 +913,6 @@ private System.Threading.Timer? updateTimer;
auction.MaxPrice = settings.DefaultMaxPrice; auction.MaxPrice = settings.DefaultMaxPrice;
settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})"; settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})";
break; 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++; 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>? OnLog;
public event Action<string>? OnResetCountChanged; public event Action<string>? OnResetCountChanged;
// Throttling per log di blocco (evita spam nel log globale)
private readonly Dictionary<string, DateTime> _lastBlockLogTime = new();
/// <summary> /// <summary>
/// Evento fired quando un'asta termina (vinta, persa o chiusa). /// Evento fired quando un'asta termina (vinta, persa o chiusa).
/// Parametri: AuctionInfo, AuctionState finale, bool won (true se vinta dall'utente) /// Parametri: AuctionInfo, AuctionState finale, bool won (true se vinta dall'utente)
@@ -185,10 +188,8 @@ namespace AutoBidder.Services
auction.MinPrice = minPrice; auction.MinPrice = minPrice;
auction.MaxPrice = maxPrice; 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; return true;
} }
} }
@@ -210,8 +211,6 @@ namespace AutoBidder.Services
{ {
auction.MinPrice = minPrice; auction.MinPrice = minPrice;
auction.MaxPrice = maxPrice; auction.MaxPrice = maxPrice;
auction.MinResets = minResets;
auction.MaxResets = maxResets;
count++; count++;
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}"); 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 tickerIntervalMs = Math.Max(100, settings.TickerIntervalMs); // Minimo 100ms
int pollingIntervalMs = 500; // Poll API ogni 500ms max int pollingIntervalMs = 500; // Poll API ogni 500ms max
DateTime lastPoll = DateTime.MinValue; DateTime lastPoll = DateTime.MinValue;
DateTime lastDiagnostic = DateTime.MinValue;
OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms"); OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms");
@@ -279,42 +279,128 @@ namespace AutoBidder.Services
continue; continue;
} }
// === FASE 2: Poll API solo ogni pollingIntervalMs === // === DIAGNOSTICA PERIODICA (ogni 30s) ===
var now = DateTime.UtcNow; var nowDiag = DateTime.UtcNow;
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs; if ((nowDiag - lastDiagnostic).TotalSeconds >= 30)
// Poll più frequente se vicino alla scadenza
bool anyNearDeadline = activeAuctions.Any(a =>
GetEstimatedTimerMs(a) < settings.StrategyCheckThresholdMs);
if (shouldPoll || anyNearDeadline)
{ {
var pollTasks = activeAuctions.Select(a => PollAuctionState(a, token)); lastDiagnostic = nowDiag;
await Task.WhenAll(pollTasks); settings = SettingsManager.Load(); // Ricarica impostazioni
lastPoll = now;
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) foreach (var auction in activeAuctions)
{ {
if (auction.IsPaused || auction.LastState == null) continue; if (auction.IsPaused || auction.LastState == null) continue;
// Calcola timer stimato LOCALMENTE (più preciso del polling)
double estimatedTimerMs = GetEstimatedTimerMs(auction); double estimatedTimerMs = GetEstimatedTimerMs(auction);
// Offset configurato dall'utente (SENZA compensazioni)
int offsetMs = auction.BidBeforeDeadlineMs > 0 int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs ? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs; : settings.DefaultBidBeforeDeadlineMs;
// TRIGGER: Timer <= Offset configurato dall'utente if (estimatedTimerMs <= offsetMs && estimatedTimerMs > -tickerIntervalMs)
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > 0)
{ {
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); await Task.Delay(tickerIntervalMs, token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -345,9 +431,11 @@ namespace AutoBidder.Services
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds; var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
// Timer stimato = timer raw - tempo trascorso // 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; double estimated = auction.LastRawTimer - elapsed;
return Math.Max(0, estimated); return estimated;
} }
/// <summary> /// <summary>
@@ -388,12 +476,19 @@ namespace AutoBidder.Services
EnsureCurrentBidInHistory(auction, state); 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 || if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost || state.Status == AuctionStatus.EndedLost ||
state.Status == AuctionStatus.Closed) 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; return;
} }
@@ -405,6 +500,15 @@ namespace AutoBidder.Services
auction.LastState = state; 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) if (shouldNotify)
{ {
OnAuctionUpdated?.Invoke(state); OnAuctionUpdated?.Invoke(state);
@@ -538,31 +642,64 @@ namespace AutoBidder.Services
return false; 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> /// <summary>
/// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato. /// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato.
/// NESSUNA compensazione automatica della latenza - l'utente decide il timing esatto. /// NESSUNA compensazione automatica della latenza - l'utente decide il timing esatto.
/// </summary> /// </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; var state = auction.LastState;
if (state == null || state.IsMyBid || estimatedTimerMs <= 0) return; if (state == null || state.IsMyBid || estimatedTimerMs <= 0) return;
// Log timing se abilitato // Log timing dettagliato per ogni check del ticker
if (settings.LogTiming) auction.AddLog($"Timer={estimatedTimerMs:F0}ms | Offset={offsetMs}ms | Prezzo=€{state.Price:F2} | Ultimo={state.LastBidder ?? "-"}",
{ Models.AuctionLogLevel.Timing, Models.AuctionLogCategory.Ticker);
auction.AddLog($"[TICKER] Timer stimato={estimatedTimerMs:F0}ms <= Offset={offsetMs}ms");
}
// === PROTEZIONE DOPPIA PUNTATA === // === PROTEZIONE DOPPIA PUNTATA ===
// Reset se timer è aumentato (qualcuno ha puntato = nuovo ciclo)
if (estimatedTimerMs > auction.LastScheduledTimerMs + 500) 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; auction.BidScheduled = false;
} }
// Reset se passato troppo tempo dall'ultima puntata
if (auction.LastClickAt.HasValue && if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10) (DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
{ {
@@ -572,6 +709,8 @@ namespace AutoBidder.Services
// Skip se già schedulata per questo ciclo // Skip se già schedulata per questo ciclo
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs) < 200) 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; return;
} }
@@ -579,18 +718,23 @@ namespace AutoBidder.Services
if (auction.LastClickAt.HasValue && if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) (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; return;
} }
// === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target === // === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target ===
// Evita calcoli inutili quando siamo lontani
if (estimatedTimerMs > settings.StrategyCheckThresholdMs) if (estimatedTimerMs > settings.StrategyCheckThresholdMs)
{ {
return; return;
} }
// === CONTROLLI FONDAMENTALI === // === 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; return;
} }
@@ -601,17 +745,22 @@ namespace AutoBidder.Services
if (!decision.ShouldBid) 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; return;
} }
auction.AddLog($"✓ Tutti i controlli superati → PUNTATA!",
Models.AuctionLogLevel.Bid, Models.AuctionLogCategory.BidAttempt);
// === ESEGUI PUNTATA === // === ESEGUI PUNTATA ===
auction.BidScheduled = true; auction.BidScheduled = true;
auction.LastScheduledTimerMs = estimatedTimerMs; auction.LastScheduledTimerMs = estimatedTimerMs;
auction.AddLog($"[BID] Timer locale={estimatedTimerMs:F0}ms, Offset utente={offsetMs}ms"); 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 ? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs; : settings.DefaultBidBeforeDeadlineMs;
double timerMs = state.Timer * 1000; double timerMs = state.Timer * 1000;
await TryPlaceBidTicker(auction, timerMs, offsetMs, token); await TryPlaceBidTicker(auction, timerMs, offsetMs, settings, token);
} }
/// <summary> /// <summary>
/// Esegue la puntata e registra metriche /// Esegue la puntata e registra metriche
/// </summary> /// </summary>
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, CancellationToken token) private async Task ExecuteBid(AuctionInfo auction, AuctionState state, AppSettings settings, CancellationToken token)
{ {
try try
{ {
@@ -694,7 +843,6 @@ namespace AutoBidder.Services
else else
{ {
var pollingPing = auction.PollingLatencyMs; var pollingPing = auction.PollingLatencyMs;
var settings = SettingsManager.Load();
// Rileva errore "timer scaduto" per feedback utente // Rileva errore "timer scaduto" per feedback utente
bool isLateBid = result.Error?.Contains("timer") == true || 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) // CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili)
{
auction.AddLog($"[DEBUG] === INIZIO CONTROLLI PUNTATA ===");
}
// ?? CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili)
if (settings.ValueCheckEnabled && if (settings.ValueCheckEnabled &&
auction.BuyNowPrice.HasValue && auction.BuyNowPrice.HasValue &&
auction.BuyNowPrice.Value > 0 && auction.BuyNowPrice.Value > 0 &&
@@ -757,27 +900,26 @@ namespace AutoBidder.Services
auction.CalculatedValue.Savings.HasValue && auction.CalculatedValue.Savings.HasValue &&
!auction.CalculatedValue.IsWorthIt) !auction.CalculatedValue.IsWorthIt)
{ {
// Usa la percentuale configurabile dall'utente
if (auction.CalculatedValue.SavingsPercentage.HasValue && if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage) auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage)
{ {
// 🔥 Logga SEMPRE - è un blocco frequente e importante auction.AddLog($"⛔ Risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto",
auction.AddLog($"[VALUE] Puntata bloccata: 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; return false;
} }
} }
else
if (settings.LogTiming && settings.ValueCheckEnabled)
{ {
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" // CONTROLLO ANTI-COLLISIONE (OPZIONALE)
// DISABILITATO DI DEFAULT - può far perdere aste competitive!
if (settings.HardcodedAntiCollisionEnabled) if (settings.HardcodedAntiCollisionEnabled)
{ {
var recentBidsThreshold = 10; // secondi var recentBidsThreshold = 10;
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata var maxActiveBidders = 3;
try try
{ {
@@ -791,125 +933,108 @@ namespace AutoBidder.Services
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.Count(); .Count();
if (settings.LogTiming) auction.AddLog($"Competizione: {activeBidders} bidder attivi (soglia={maxActiveBidders})",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Competition);
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
}
if (activeBidders >= maxActiveBidders) if (activeBidders >= maxActiveBidders)
{ {
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
var session = _apiClient.GetSession(); var session = _apiClient.GetSession();
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault(); var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
if (lastBid != null && if (lastBid != null &&
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase)) !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; return false;
} }
} }
} }
catch { /* Ignora errori nel controllo competizione */ } catch { }
} }
if (settings.LogTiming) // CONTROLLO 1: Limite minimo puntate residue
{
auction.AddLog($"[DEBUG] ? Controllo competizione OK");
}
// ?? CONTROLLO 1: Limite minimo puntate residue
if (settings.MinimumRemainingBids > 0) if (settings.MinimumRemainingBids > 0)
{ {
var session = _apiClient.GetSession(); var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids) if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{ {
// 🔥 Logga SEMPRE - è un blocco importante auction.AddLog($"⛔ Puntate residue ({session.RemainingBids}) ≤ limite ({settings.MinimumRemainingBids})",
auction.AddLog($"[LIMIT] Puntata bloccata: 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; return false;
} }
else if (session != null)
if (settings.LogTiming && 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 (state.IsMyBid)
{ {
if (settings.LogTiming) auction.AddLog($"✓ Sono già vincitore corrente - skip",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.BidAttempt);
auction.AddLog($"[DEBUG] Sono già vincitore");
}
return false; 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) if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{ {
// 🔥 Logga SEMPRE questo blocco - è critico per capire perché non punta auction.AddLog($"⛔ Prezzo €{state.Price:F2} < Min €{auction.MinPrice:F2}",
auction.AddLog($"[PRICE] Prezzo troppo basso: €{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; return false;
} }
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice) if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{ {
// 🔥 Logga SEMPRE questo blocco - è critico auction.AddLog($"⛔ Prezzo €{state.Price:F2} > Max €{auction.MaxPrice:F2}",
auction.AddLog($"[PRICE] Prezzo troppo alto: €{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; return false;
} }
if (settings.LogTiming) auction.AddLog($"✓ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "")}])",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Price);
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}])");
}
}
// ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate) // CONTROLLO 6: Cooldown
if (auction.LastClickAt.HasValue) if (auction.LastClickAt.HasValue)
{ {
var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value; var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value;
if (timeSinceLastClick.TotalMilliseconds < 800) if (timeSinceLastClick.TotalMilliseconds < 800)
{ {
if (settings.LogTiming) auction.AddLog($"Cooldown: {timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker);
auction.AddLog($"[DEBUG] Cooldown attivo ({timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms)");
}
return false; return false;
} }
} }
if (settings.LogTiming) auction.AddLog($"✓ Tutti i controlli ShouldBid superati",
{ Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt);
auction.AddLog($"[DEBUG] === TUTTI I CONTROLLI SUPERATI ===");
}
return true; return true;
} }
@@ -968,7 +1093,7 @@ namespace AutoBidder.Services
{ {
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
EventType = BidEventType.Reset, EventType = BidEventType.Reset,
Bidder = state.LastBidder, Bidder = state.LastBidder ?? "",
Price = state.Price, Price = state.Price,
Timer = state.Timer, Timer = state.Timer,
Notes = $"Puntata: EUR{state.Price:F2}" Notes = $"Puntata: EUR{state.Price:F2}"

View File

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

View File

@@ -98,12 +98,14 @@ namespace AutoBidder.Services
stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice); stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice);
stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice); stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice);
stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice); stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice);
stats.MedianFinalPrice = CalculateMedian(wonResults.Select(r => r.FinalPrice).ToList());
} }
else if (results.Any()) else if (results.Any())
{ {
stats.AvgFinalPrice = results.Average(r => r.FinalPrice); stats.AvgFinalPrice = results.Average(r => r.FinalPrice);
stats.MinFinalPrice = results.Min(r => r.FinalPrice); stats.MinFinalPrice = results.Min(r => r.FinalPrice);
stats.MaxFinalPrice = results.Max(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) // 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)); double sumSquares = data.Sum(d => Math.Pow(d - avg, 2));
return Math.Sqrt(sumSquares / (data.Count - 1)); 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); 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> /// <summary>
/// Ottiene tutte le statistiche prodotto /// Ottiene tutte le statistiche prodotto
/// </summary> /// </summary>

View File

@@ -181,6 +181,14 @@ namespace AutoBidder.Utilities
/// </summary> /// </summary>
public bool AutoApplyProductDefaults { get; set; } = true; 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> /// <summary>
/// Log stato asta (terminata, reset, ecc.) [STATUS] /// Log stato asta (terminata, reset, ecc.) [STATUS]
/// Default: true /// 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 _folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
private static readonly string _file = Path.Combine(_folder, "settings.json"); 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() public static AppSettings Load()
{ {
try lock (_cacheLock)
{ {
if (!File.Exists(_file)) return new AppSettings(); var now = DateTime.UtcNow;
var txt = File.ReadAllText(_file); if (_cached != null && now < _cacheExpiry)
var s = JsonSerializer.Deserialize<AppSettings>(txt); return _cached;
if (s == null) return new AppSettings();
return s; 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) public static void Save(AppSettings settings)
@@ -448,6 +479,13 @@ namespace AutoBidder.Utilities
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder); if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }); var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_file, txt); 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 { } catch { }
} }

View File

@@ -1303,3 +1303,181 @@
.btn-xs i { .btn-xs i {
font-size: 0.75rem; 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);
}