Files
Mimante/Mimante/Models/AuctionInfo.cs
T
Alby96andClaude Fable 5.1 9177b31bd5 Due database, interruttore dell'apprendimento, barre a icone; via kill-switch, esportazioni e voci deprecate
Tutto ciò che l'applicazione registra sta ora in due file SQLite in
%LocalAppData%\AutoBidder\Database: autobidder.sqlite per le osservazioni e
esercizio.sqlite per i documenti d'esercizio (aste nel monitor, prodotti, promozioni,
modelli appresi con prefisso ml/) e per i registri applicativo e del riscatto puntate.
Il motore comune è SqliteDatabase; i file JSON e la cartella Apprendimento delle
versioni precedenti vengono importati la prima volta e lasciati dove sono. La cartella
si cambia dalle Impostazioni: al salvataggio si chiede se spostare i file, e si riavvia.
Le cartelle Dati/Statistiche/Registri, i registri su file, le misure dell'anticipo e
le impostazioni morte (LogBids, AutoApplyProductDefaults, NewAuctionLimitsPriority…)
non esistono più.

Il kill-switch se ne va: al suo posto un solo interruttore in barra, accanto ad
Avvia/Osserva/Ferma, che spegne l'apprendimento. Spento, il motore punta solo entro i
limiti dell'utente (prezzo, puntate, budget, fascia oraria, rischio) con l'anticipo
fisso: niente valore atteso, regime, duello, bandit, anticipo adattivo. Le aste vengono
comunque registrate e studiate.

Tre difetti visti osservando il motore dal vivo per un'ora, in Osserva:
- la copertura «Compralo Ora» con V lasciato al listino azzerava la perdita coperta e
  il motore diceva BID a ogni scadenza con P dell'1%: ora vale solo per i prodotti con
  «Valore reale €» scritto, altrimenti la puntata si conta persa e P decide;
- la chiusura di un'asta arrivava due volte (fine, poi rimozione dal monitor) e di
  nuovo al riavvio: modello, profilo, bandit e statistiche per prodotto contavano ogni
  asta due volte. Il monitor la comunica una volta sola, l'apprendimento salta le aste
  già apprese, e le schede prodotto vengono ricostruite una volta dallo storico;
- la pagina delle ricompense veniva scambiata per la pagina di accesso perché porta un
  collegamento a /login.php: prima si guarda se è la pagina delle ricompense.

Interfaccia: la scheda Esporta e tutto il suo codice sono tolti; le esportazioni
chiedono sempre dove salvare. Prodotti, Storico e Apprendimento hanno barre a sole
icone con tooltip che dicono esattamente cosa succede, colorate come il monitor, con
i nuovi pulsanti di pulizia (spegni tutte le stelline, togli i non seguiti, pulizia
completa; seleziona tutte / elimina le selezionate / svuota lo storico). Impostazioni:
sezione Database, Manutenzione con «Elimina tutti i dati (tranne la login)» e
«Impostazioni di fabbrica», esportazione dei registri in testo. Modifiche.txt, il
prompt ormai realizzato, esce dal repository.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 13:58:08 +02:00

872 lines
35 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;
/// <summary>
/// L'anticipo di quest'asta è stato scritto a mano dall'utente. Solo allora
/// <see cref="BidBeforeDeadlineMs"/> comanda: altrimenti il cecchino lo lascia
/// decidere al modello di latenza (vedi Ml/LatencyModel) e scrive qui il valore
/// scelto, così la griglia mostra l'anticipo davvero in uso.
/// </summary>
public bool BidLeadIsManual { get; set; }
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;
// ── Duello con l'autopuntata avversaria ──────────────────────────
// Vedi Utilities/AutoBidDuel per il perche' e per i numeri misurati.
/// <summary>Istante dell'ultima puntata nostra, per misurare quanto tarda la risposta.</summary>
[JsonIgnore]
public DateTime? LastMyBidAtUtc { get; set; }
/// <summary>
/// Risposte avversarie di fila con la firma dell'autopuntata (~6 s). Oltre la
/// soglia si smette di puntare su quest'asta: vedi <see cref="AutoBidDuelDetected"/>.
/// </summary>
[JsonIgnore]
public int AutoResponsesInARow { get; set; }
/// <summary>Riconosciuta un'autopuntata armata dall'altra parte.</summary>
[JsonIgnore]
public bool AutoBidDuelDetected { get; set; }
/// <summary>
/// Il regime di concorrenza di quest'asta: quando lasciar sfogare gli altri e
/// quando tornare a puntare. Vedi <see cref="Ml.CompetitionRegime"/>.
/// Stato di sessione: al riavvio riparte Calmo, e va bene così.
/// </summary>
[JsonIgnore]
public Ml.CompetitionRegime Regime { get; } = new();
// ── Apprendimento: l'ultima risposta del modello per quest'asta ──
// Vedi Ml/LearningService. Non si salvano: valgono per l'istante in cui sono state
// calcolate, e al riavvio non ci sarebbe piu' lo stato che le ha prodotte.
/// <summary>
/// La decisione registrata nel database per il ciclo in corso: la puntata che
/// parte porta questo id, così esito e decisione restano legati.
/// </summary>
[JsonIgnore]
public long? PendingDecisionId { get; set; }
/// <summary>Probabilità che una puntata fatta adesso resti senza risposta.</summary>
[JsonIgnore]
public double? LearnedUnansweredProbability { get; set; }
/// <summary>Valore atteso in euro di una puntata fatta adesso: sotto zero si perde.</summary>
[JsonIgnore]
public double? LearnedExpectedValue { get; set; }
/// <summary>
/// Registra la risposta avversaria arrivata adesso e aggiorna il conteggio.
/// Restituisce true se proprio con questa risposta il duello viene riconosciuto,
/// cosi' chi chiama puo' scriverlo nel registro una volta sola.
/// </summary>
public bool NoteOpponentResponse(DateTime nowUtc, int soglia)
{
if (LastMyBidAtUtc is not { } miaUltima) return false;
var ritardo = (nowUtc - miaUltima).TotalSeconds;
// Consumata: la prossima risposta va misurata dalla prossima puntata nostra.
LastMyBidAtUtc = null;
AutoResponsesInARow = Utilities.AutoBidDuel.Aggiorna(AutoResponsesInARow, ritardo);
// Il regime vuole sapere se una puntata di prova è stata coperta subito.
Regime.RispostaAvversaria(ritardo);
if (AutoBidDuelDetected) return false;
if (!Utilities.AutoBidDuel.DuelloRiconosciuto(AutoResponsesInARow, soglia)) return false;
AutoBidDuelDetected = true;
return true;
}
/// <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>
/// 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>
/// La chiusura è già stata comunicata a storico, statistiche e apprendimento.
/// Si salva con l'asta: un'asta conclusa che resta in elenco fino al riavvio non
/// deve essere contata di nuovo.
/// </summary>
public bool CompletionReported { get; set; }
/// <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 jitter per questa asta
/// </summary>
public bool? JitterEnabledOverride { get; set; }
/// <summary>
/// Override per questa asta della sospensione a fascia oraria.
/// <c>null</c> = come dice il prodotto, e se il prodotto tace come dicono le
/// impostazioni globali. Vedi <see cref="Utilities.BiddingHours"/>.
/// </summary>
public bool? QuietHoursEnabledOverride { get; set; }
/// <summary>
/// Override: limite puntate per questa asta
/// </summary>
public int? MaxBidsOverride { get; set; }
// ?? NUOVO: Rilevamento situazione di duello
// ???????????????????????????????????????????????????????????????????
// 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!;
// Pulisci oggetti complessi
LastState = null;
CalculatedValue = 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; } = "";
}
}