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>
872 lines
35 KiB
C#
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; } = "";
|
|
}
|
|
}
|