- 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.
855 lines
33 KiB
C#
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; } = "";
|
|
}
|
|
}
|