Files
Mimante/Mimante/Models/AuctionInfo.cs
T
Alby96 7ca504a70a Add utility classes for theme management, waiting, watched products, and notifications
- Implement ThemeManager for dynamic light/dark theme switching in the application.
- Create Wait class for cancellable delays without exceptions for smoother user experience.
- Introduce WatchedProductsStore to manage and persist watched products in JSON format.
- Add WindowsNotifier for system notifications to inform users of important events.
- Develop ProductViewModel to encapsulate product data and manage UI interactions effectively.
2026-08-04 21:49:53 +02:00

855 lines
33 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace AutoBidder.Models
{
/// <summary>
/// Stato operativo di un'asta nel monitor.
/// </summary>
public enum RunState
{
/// <summary>Ferma: nessuna richiesta verso Bidoo.</summary>
Stopped,
/// <summary>Osserva: segue prezzo, scadenza e avversari, ma non punta mai.</summary>
Watch,
/// <summary>Attiva: segue e punta secondo le regole configurate.</summary>
Active
}
/// <summary>
/// Informazioni base di un'asta monitorata
/// Solo HTTP, nessuna modalit, browser o multi-click
/// </summary>
public class AuctionInfo
{
/// <summary>
/// Numero massimo di righe di log da mantenere per ogni asta
/// Ridotto per ottimizzare consumo RAM
/// </summary>
private const int MAX_LOG_LINES = 200;
public string AuctionId { get; set; } = "";
public string Name { get; set; } = ""; // Opzionale, pu essere lasciato vuoto
public string OriginalUrl { get; set; } = ""; // URL completo dell'asta (per referer)
// Configurazione asta
/// <summary>
/// Millisecondi prima della scadenza (deadline) per inviare la puntata.
/// Es: 200ms = punta 200ms prima che il timer raggiunga 0.
/// </summary>
public int BidBeforeDeadlineMs { get; set; } = 200;
public double MinPrice { get; set; } = 0;
public double MaxPrice { get; set; } = 0;
public int MinResets { get; set; } = 0; // Numero minimo reset prima di puntare
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
/// <summary>
/// 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>
[JsonPropertyName("MaxClicks")]
public int MaxClicks { get; set; } = 0;
/// <summary>
/// Tetto di spesa in euro per questa asta, contando solo le mie puntate
/// (0 = nessun tetto). È il fratello di <see cref="MaxClicks"/> espresso in denaro
/// invece che in conteggio: chi ragiona in euro non deve fare la divisione a mente.
/// </summary>
public double MaxTotalSpendEuro { get; set; } = 0;
/// <summary>
/// Fermati quando vincere costerebbe più di quanto vale il prodotto
/// (prezzo + puntate spese + spedizione contro il "Compra Subito").
///
/// <para>Acceso di predefinito: è il controllo che distingue "vincere" da "vincere
/// senza perderci". Si può spegnere per un articolo che si vuole comunque, a
/// qualunque prezzo.</para>
/// </summary>
public bool StopAtBreakEven { get; set; } = true;
// Stato asta
// IsActive/IsPaused restano la fonte serializzata (retrocompatibilità con
// auctions.json e con i binding della griglia); State ne è la lettura semantica.
public bool IsActive { get; set; } = true;
public bool IsPaused { get; set; } = false;
/// <summary>
/// Stato operativo dell'asta. È una vista su IsActive/IsPaused, non un campo a parte:
/// così non esistono due verità da tenere allineate.
///
/// Ferma = nessuna richiesta; Osserva = segue prezzo e scadenza ma non punta mai;
/// Attiva = segue e punta secondo le regole configurate.
/// </summary>
[JsonIgnore]
public RunState State
{
get => !IsActive ? RunState.Stopped : IsPaused ? RunState.Watch : RunState.Active;
set
{
IsActive = value != RunState.Stopped;
IsPaused = value == RunState.Watch;
}
}
// Contatori
public int ResetCount { get; set; } = 0;
/// <summary>
/// Puntate residue totali dell'utente (aggiornate dopo ogni puntata su questa asta)
/// </summary>
[JsonPropertyName("RemainingBids")]
public int? RemainingBids { get; set; }
/// <summary>
/// Puntate usate specificamente su questa asta (da risposta server)
/// </summary>
[JsonPropertyName("BidsUsedOnThisAuction")]
public int? BidsUsedOnThisAuction { get; set; }
// Timestamp
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastClickAt { get; set; }
// ?? NUOVO: Sistema timing basato su deadline
/// <summary>
/// Scadenza del ciclo corrente in tick di Stopwatch, ancorata all'orologio del server.
/// È il bersaglio su cui il cecchino calcola l'istante di fuoco: a differenza di
/// DeadlineUtc non dipende dall'orologio locale né dalla granularità del secondo.
/// 0 = non ancora nota.
/// </summary>
[JsonIgnore]
public long DeadlineTicks { get; set; }
/// <summary>Numero di poll eseguiti su questa asta nella sessione corrente.</summary>
[JsonIgnore]
public long PollCount { get; set; }
/// <summary>Poll falliti (nessuna risposta utilizzabile) nella sessione corrente.</summary>
[JsonIgnore]
public long PollErrors { get; set; }
/// <summary>
/// Risposte consecutive che dichiarano l'asta conclusa. Ne servono due prima di
/// chiudere davvero: un singolo OFF isolato è quasi sempre un artefatto del server.
/// </summary>
[JsonIgnore]
public int TerminalStreak { get; set; }
/// <summary>
/// Andamento del prezzo mentre l'asta è in corso, campionato a ogni variazione.
///
/// Va raccolto adesso perché a posteriori non esiste: Bidoo non espone lo storico
/// dei prezzi di un'asta conclusa. È il dato più utile per distinguere un'asta
/// tranquilla da una contesa.
/// </summary>
[JsonIgnore]
public List<PricePoint> PriceSeries { get; } = new();
/// <summary>Quando il monitor ha visto per la prima volta questa asta.</summary>
[JsonIgnore]
public DateTime? FirstSeenAt { get; set; }
/// <summary>Numero massimo di campioni di prezzo: un'asta lunga non deve gonfiare la memoria.</summary>
private const int MaxPricePoints = 600;
/// <summary>Registra un prezzo, ma solo quando cambia davvero.</summary>
public void TrackPrice(double price)
{
if (price <= 0) return;
FirstSeenAt ??= DateTime.Now;
lock (PriceSeries)
{
if (PriceSeries.Count > 0 && Math.Abs(PriceSeries[^1].Price - price) < 0.001) return;
if (PriceSeries.Count >= MaxPricePoints)
{
// Si dimezza la risoluzione tenendo un campione su due: la forma della
// curva resta leggibile e il costo in memoria smette di crescere.
var thinned = PriceSeries.Where((_, i) => i % 2 == 0).ToList();
PriceSeries.Clear();
PriceSeries.AddRange(thinned);
}
PriceSeries.Add(new PricePoint
{
T = (DateTime.Now - FirstSeenAt.Value).TotalSeconds,
Price = price
});
}
}
/// <summary>Copia della serie prezzi, utilizzabile fuori dal lock.</summary>
public List<PricePoint> SnapshotPriceSeries()
{
lock (PriceSeries) return PriceSeries.ToList();
}
/// <summary>
/// Millisecondi stimati alla scadenza, dedotti dal contatore monotono.
/// double.MaxValue quando la scadenza non è ancora nota.
/// </summary>
public double EstimatedRemainingMs()
{
if (DeadlineTicks == 0) return double.MaxValue;
return AutoBidder.Engine.PrecisionWait.MsUntil(DeadlineTicks);
}
// Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
/// <summary>
/// Esito dell'asta secondo lo storico interno: <c>"won"</c>, <c>"lost"</c>,
/// <c>"closed"</c>, oppure null se non risulta conclusa.
///
/// A differenza di <see cref="LastState"/> questo dato è serializzato, quindi
/// sopravvive alla chiusura dell'applicazione. È la <b>stessa</b> fonte che usa il
/// motore per decidere se accendere un runner: senza, l'interfaccia poteva mostrare
/// "Osserva" su un'asta che il motore considerava già finita e non interrogava più.
/// </summary>
[JsonIgnore]
public string? ConcludedOutcome
{
get
{
var notes = BidHistory != null && BidHistory.Count > 0
? BidHistory[^1].Notes
: null;
if (string.IsNullOrEmpty(notes)) return null;
if (notes.Contains("VINTA", StringComparison.OrdinalIgnoreCase)) return "won";
if (notes.Contains("Persa", StringComparison.OrdinalIgnoreCase)) return "lost";
if (notes.Contains("Chiusa", StringComparison.OrdinalIgnoreCase)) return "closed";
return null;
}
}
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Storia delle ultime puntate effettuate sull'asta (da API)
/// Questa la fonte UFFICIALE per il conteggio puntate per utente
/// </summary>
[JsonPropertyName("RecentBids")]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>();
/// <summary>
/// Oggetto di sincronizzazione per <see cref="RecentBids"/>.
///
/// Serve un lucchetto <b>separato</b> perché la lista viene sostituita (ordinata,
/// troncata) mentre la si aggiorna: bloccare la lista stessa significherebbe che il
/// thread successivo blocca un oggetto diverso, e la mutua esclusione sparisce.
/// Il motore la scrive dal proprio thread, l'interfaccia la legge dal suo.
/// </summary>
[JsonIgnore]
public object BidsLock { get; } = new();
/// <summary>
/// Copia della cronologia utilizzabile fuori dal lock. È l'unico modo corretto per
/// leggerla dall'interfaccia mentre il motore continua ad aggiornarla.
/// </summary>
public List<BidHistoryEntry> SnapshotRecentBids(int max = 0)
{
lock (BidsLock)
{
if (RecentBids == null || RecentBids.Count == 0) return new List<BidHistoryEntry>();
return max > 0 && RecentBids.Count > max
? RecentBids.Take(max).ToList()
: RecentBids.ToList();
}
}
/// <summary>
/// Copia delle statistiche per puntatore. Sono derivate da <see cref="RecentBids"/>
/// e aggiornate insieme a essa, quindi condividono lo stesso lucchetto: enumerare il
/// dizionario mentre il motore lo riscrive sollevrebbe un'eccezione.
/// </summary>
public List<BidderInfo> SnapshotBidderStats()
{
lock (BidsLock)
{
if (BidderStats == null || BidderStats.Count == 0) return new List<BidderInfo>();
return BidderStats.Values.OrderByDescending(b => b.BidCount).ToList();
}
}
// Log per-asta strutturato (non serializzato)
[System.Text.Json.Serialization.JsonIgnore]
public List<AuctionLogEntry> AuctionLog { get; set; } = new();
/// <summary>
/// Osservatore globale delle righe di registro delle aste. Serve al dossier su file:
/// le righe nascono in una quarantina di punti diversi del motore, e agganciarsi qui
/// è l'unico modo per prenderle tutte senza dover ricordare di scrivere due volte
/// ogni volta che se ne aggiunge una.
///
/// <para>Statico perché il modello non conosce né i percorsi né le impostazioni: chi
/// lo collega decide dove finiscono le righe, e se non lo collega nessuno non
/// succede nulla.</para>
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
public static Action<AuctionInfo, AuctionLogEntry>? LogSink { get; set; }
private void PublishToSink(AuctionLogEntry entry)
{
try { LogSink?.Invoke(this, entry); }
catch { /* un osservatore rotto non deve fermare il motore */ }
}
// Flag runtime: indica che in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore]
public bool IsAttackInProgress { get; set; } = false;
// === INFORMAZIONI PRODOTTO PER CALCOLO VALORE ===
/// <summary>
/// Prezzo "Compra Subito" del prodotto (valore nominale)
/// </summary>
public double? BuyNowPrice { get; set; }
/// <summary>
/// Spese di spedizione per questo prodotto
/// </summary>
public double? ShippingCost { get; set; }
/// <summary>
/// Indica se c' un limite di vincita per questo prodotto (1 volta ogni X giorni)
/// </summary>
public bool HasWinLimit { get; set; } = false;
/// <summary>
/// Descrizione del limite di vincita (es: "1 volta ogni 30 giorni")
/// </summary>
public string? WinLimitDescription { get; set; }
/// <summary>
/// Costo medio per puntata da utilizzare nei calcoli (default: 0.20)
/// </summary>
public double BidCost { get; set; } = 0.20;
/// <summary>
/// Ultimo valore calcolato del prodotto (costo reale considerando puntate)
/// Null se non ancora calcolato
/// </summary>
[JsonIgnore]
public ProductValue? CalculatedValue { get; set; }
/// <summary>
/// Ultimo stato ricevuto dal monitor per questa asta
/// </summary>
[JsonIgnore]
public AuctionState? LastState { get; set; }
/// <summary>
/// Aggiunge una voce strutturata al log dell'asta con deduplicazione e limite righe.
/// Parsifica automaticamente il tag [TAG] per determinare livello e categoria.
/// </summary>
public void AddLog(string message, int maxLines = 200)
{
// Protezione null-safety (dopo ClearData)
if (AuctionLog == null) AuctionLog = new();
var now = DateTime.Now;
// 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 last = AuctionLog[^1];
if (last.Message == cleanMessage && last.Category == category)
{
last.RepeatCount++;
last.Timestamp = now;
return;
}
}
var entry = new AuctionLogEntry
{
Timestamp = now,
Level = level,
Category = category,
Message = cleanMessage
};
AuctionLog.Add(entry);
PublishToSink(entry);
if (AuctionLog.Count > maxLines)
{
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;
}
}
var entry = new AuctionLogEntry
{
Timestamp = now,
Level = level,
Category = category,
Message = message
};
AuctionLog.Add(entry);
PublishToSink(entry);
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
// ???????????????????????????????????????????????????????????????
// TRACKING AVANZATO PER STRATEGIE
// ???????????????????????????????????????????????????????????????
/// <summary>
/// Storico latenze ultime N misurazioni (per media mobile)
/// </summary>
[JsonIgnore]
public List<int> LatencyHistory { get; set; } = new();
/// <summary>
/// Numero massimo di latenze da memorizzare (ridotto per RAM)
/// </summary>
private const int MAX_LATENCY_HISTORY = 10;
/// <summary>
/// Aggiunge una misurazione di latenza allo storico
/// </summary>
public void AddLatencyMeasurement(int latencyMs)
{
LatencyHistory.Add(latencyMs);
if (LatencyHistory.Count > MAX_LATENCY_HISTORY)
LatencyHistory.RemoveAt(0);
PollingLatencyMs = latencyMs;
}
/// <summary>
/// Latenza media calcolata sullo storico
/// </summary>
[JsonIgnore]
public double AverageLatencyMs => LatencyHistory.Count > 0
? LatencyHistory.Average()
: PollingLatencyMs > 0 ? PollingLatencyMs : 60;
/// <summary>
/// Heat metric (0-100) che indica quanto "calda" l'asta
/// Calcolato in base a: bidder attivi, frequenza puntate, collisioni
/// </summary>
[JsonIgnore]
public int HeatMetric { get; set; } = 0;
/// <summary>
/// Numero di bidder unici attivi negli ultimi N secondi
/// </summary>
[JsonIgnore]
public int ActiveBiddersCount { get; set; } = 0;
/// <summary>
/// Numero di collisioni rilevate (puntate nello stesso secondo)
/// </summary>
[JsonIgnore]
public int CollisionCount { get; set; } = 0;
/// <summary>
/// Collisioni consecutive senza puntata vincente
/// </summary>
[JsonIgnore]
public int ConsecutiveCollisions { get; set; } = 0;
/// <summary>
/// Timestamp dell'ultimo soft retreat
/// </summary>
[JsonIgnore]
public DateTime? LastSoftRetreatAt { get; set; }
/// <summary>
/// Se true, l'asta in soft retreat temporaneo
/// </summary>
[JsonIgnore]
public bool IsInSoftRetreat { get; set; } = false;
/// <summary>
/// Contatore puntate effettuate in questa sessione su questa asta
/// </summary>
[JsonIgnore]
public int SessionBidCount { get; set; } = 0;
/// <summary>
/// Numero di volte che il timer scaduto prima della puntata
/// </summary>
[JsonIgnore]
public int TimerExpiredCount { get; set; } = 0;
/// <summary>
/// Numero di puntate riuscite
/// </summary>
[JsonIgnore]
public int SuccessfulBidCount { get; set; } = 0;
/// <summary>
/// Numero di puntate fallite
/// </summary>
[JsonIgnore]
public int FailedBidCount { get; set; } = 0;
/// <summary>
/// Lista utenti identificati come aggressivi in questa asta
/// </summary>
[JsonIgnore]
public HashSet<string> AggressiveBidders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Offset dinamico calcolato per questa asta (ms)
/// </summary>
[JsonIgnore]
public int DynamicOffsetMs { get; set; } = 150;
/// <summary>
/// Offset effettivo usato nell'ultima puntata (include jitter)
/// </summary>
[JsonIgnore]
public int LastUsedOffsetMs { get; set; } = 0;
/// <summary>
/// Indica se questa asta stata seguita dall'inizio (per salvare storia completa)
/// </summary>
public bool IsTrackedFromStart { get; set; } = false;
/// <summary>
/// True se il monitor c'era <b>prima che l'asta partisse</b>: l'ha vista programmata,
/// oppure al primo centesimo, cioè prima di qualunque puntata.
///
/// <para>Da non confondere con <see cref="IsTrackedFromStart"/>, che dice soltanto
/// "il monitoraggio è cominciato": un'asta aggiunta a metà corsa lo mette a true lo
/// stesso. Qui invece la domanda è un'altra — questi dati raccontano tutta la storia
/// o solo la fine? — ed è quella che decide se un'asta vale come materiale di
/// analisi.</para>
/// </summary>
public bool ObservedFromStart { get; set; }
/// <summary>True se la conclusione è stata vista dal vivo, non dedotta dopo.</summary>
public bool ObservedToEnd { get; set; }
/// <summary>
/// Quando l'asta è finita davvero.
///
/// <para>Serve perché il momento in cui si registra un'asta conclusa non coincide
/// con quello in cui è finita: un'asta terminata resta nel monitor finché non la si
/// toglie, e senza questo campo lo storico segnerebbe come data di chiusura l'ora
/// della rimozione — magari il giorno dopo.</para>
/// </summary>
public DateTime? ConcludedAt { get; set; }
/// <summary>
/// Timestamp di inizio tracking
/// </summary>
public DateTime? TrackingStartedAt { get; set; }
// ???????????????????????????????????????????????????????????????
// IMPOSTAZIONI PER-ASTA (override globali)
// ???????????????????????????????????????????????????????????????
/// <summary>
/// Override: abilita/disabilita strategie avanzate per questa asta
/// null = usa impostazione globale
/// </summary>
public bool? AdvancedStrategiesEnabled { get; set; }
/// <summary>
/// Override: abilita/disabilita jitter per questa asta
/// </summary>
public bool? JitterEnabledOverride { get; set; }
/// <summary>
/// Override: abilita/disabilita soft retreat per questa asta
/// </summary>
public bool? SoftRetreatEnabledOverride { get; set; }
/// <summary>
/// Override: limite puntate per questa asta
/// </summary>
public int? MaxBidsOverride { get; set; }
// ?? NUOVO: Rilevamento situazione di duello
/// <summary>
/// True se rilevata situazione di duello (solo 2 bidder dominanti)
/// </summary>
[JsonIgnore]
public bool IsDuelSituation { get; set; } = false;
/// <summary>
/// Username dell'avversario in caso di duello
/// </summary>
[JsonIgnore]
public string? DuelOpponent { get; set; }
/// <summary>
/// Vantaggio/svantaggio nel duello (% puntate mie - % puntate avversario)
/// Positivo = sto dominando, Negativo = sto perdendo
/// </summary>
[JsonIgnore]
public double DuelAdvantage { get; set; } = 0;
// ???????????????????????????????????????????????????????????????????
// GESTIONE MEMORIA
// ???????????????????????????????????????????????????????????????????
/// <summary>
/// Pulisce tutti i dati in memoria dell'asta per liberare RAM.
/// Chiamare prima di rimuovere l'asta dalla lista.
/// </summary>
public void ClearData()
{
// Pulisci liste storiche
BidHistory?.Clear();
BidHistory = null!;
RecentBids?.Clear();
RecentBids = null!;
AuctionLog?.Clear();
AuctionLog = null!;
BidderStats?.Clear();
BidderStats = null!;
LatencyHistory?.Clear();
LatencyHistory = null!;
AggressiveBidders?.Clear();
AggressiveBidders = null!;
// Pulisci oggetti complessi
LastState = null;
CalculatedValue = null;
DuelOpponent = null;
WinLimitDescription = null;
// Reset flag
IsTrackedFromStart = false;
TrackingStartedAt = null;
DeadlineTicks = 0;
}
/// <summary>
/// Compatta i dati mantenendo solo le informazioni recenti.
/// Utile per ridurre la memoria senza eliminare completamente i dati.
/// </summary>
public void CompactData(int maxBidHistory = 50, int maxRecentBids = 30, int maxLogLines = 100)
{
// Compatta BidHistory
if (BidHistory != null && BidHistory.Count > maxBidHistory)
{
var recent = BidHistory.TakeLast(maxBidHistory).ToList();
BidHistory.Clear();
BidHistory.AddRange(recent);
BidHistory.TrimExcess();
}
// Compatta RecentBids
if (RecentBids != null && RecentBids.Count > maxRecentBids)
{
var recent = RecentBids.TakeLast(maxRecentBids).ToList();
RecentBids.Clear();
RecentBids.AddRange(recent);
RecentBids.TrimExcess();
}
// Compatta AuctionLog
if (AuctionLog != null && AuctionLog.Count > maxLogLines)
{
var recent = AuctionLog.TakeLast(maxLogLines).ToList();
AuctionLog.Clear();
AuctionLog.AddRange(recent);
AuctionLog.TrimExcess();
}
// Compatta LatencyHistory
if (LatencyHistory != null && LatencyHistory.Count > 10)
{
var recent = LatencyHistory.TakeLast(10).ToList();
LatencyHistory.Clear();
LatencyHistory.AddRange(recent);
LatencyHistory.TrimExcess();
}
// Compatta BidderStats - mantieni solo i top bidders
if (BidderStats != null && BidderStats.Count > 20)
{
var topBidders = BidderStats
.OrderByDescending(kv => kv.Value.BidCount)
.Take(20)
.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
BidderStats.Clear();
foreach (var kv in topBidders)
BidderStats[kv.Key] = kv.Value;
}
}
}
/// <summary>
/// Rappresenta il valore calcolato di un prodotto all'asta
/// </summary>
public class ProductValue
{
/// <summary>
/// Prezzo attuale dell'asta in euro
/// </summary>
public double CurrentPrice { get; set; }
/// <summary>
/// Numero totale di puntate effettuate sull'asta
/// </summary>
public int TotalBids { get; set; }
/// <summary>
/// Numero di puntate effettuate dall'utente
/// </summary>
public int MyBids { get; set; }
/// <summary>
/// Costo delle puntate dell'utente (MyBids BidCost)
/// </summary>
public double MyBidsCost { get; set; }
/// <summary>
/// Costo totale per l'utente se vince (CurrentPrice + MyBidsCost + ShippingCost)
/// </summary>
public double TotalCostIfWin { get; set; }
/// <summary>
/// Prezzo "Compra Subito" del prodotto
/// </summary>
public double? BuyNowPrice { get; set; }
/// <summary>
/// Spese di spedizione
/// </summary>
public double? ShippingCost { get; set; }
/// <summary>
/// Risparmio rispetto al prezzo "Compra Subito" (pu essere negativo)
/// </summary>
public double? Savings { get; set; }
/// <summary>
/// Percentuale di risparmio rispetto al prezzo "Compra Subito"
/// </summary>
public double? SavingsPercentage { get; set; }
/// <summary>
/// Indica se conviene continuare (risparmio positivo)
/// </summary>
public bool IsWorthIt { get; set; }
/// <summary>
/// Timestamp del calcolo
/// </summary>
public DateTime CalculatedAt { get; set; }
/// <summary>
/// Messaggio riassuntivo del valore
/// </summary>
public string Summary { get; set; } = "";
}
}