Via le due modalita' Addestramento/Gara, per scelta dell'utente: la misura aveva detto che il lavoro di registrazione costa mezzo punto di un core, e allora tanto vale registrare sempre. Resta la coalescenza verso l'interfaccia, che era la correzione vera. COSA IMPARA. Per ogni puntata di ogni asta chiusa: e' rimasta senza risposta? E' l'unica etichetta abbondante — 1,8 milioni di esempi sui dossier locali — contro le diciannove aste vinte dall'utente, che non farebbero un modello. Il comportamento avversario non dipende da chi ha puntato, e questo rende utilizzabili le puntate altrui. Regressione logistica online scritta a mano, senza librerie, in Ml/: un passo di gradiente per esempio, aggiornata a ogni chiusura, salvata subito. Variabili a fasce: durata del ciclo, quota di autopuntate recenti, puntatori distinti, prezzo sul valore, fascia oraria (le fasce ricalcano la curva misurata sulle chiusure), feriale/festivo, ora di aggancio, tipo di puntata, profondita' dell'asta, stesso puntatore di due fa, prodotto in 32 cassetti con hash stabile. In parallelo un profilo per prodotto x fascia oraria x tipo di giorno, con restringimento verso il prodotto e poi verso il globale dove i dati scarseggiano. COME DECIDE. In ShouldPlaceBid: probabilita' appresa per il margine residuo meno il costo della puntata; sotto zero non si punta, e il registro riporta i numeri. Il modello parla sempre ma decide solo dopo 300 aste apprese. Le previsioni sono calibrate in linea con il rapporto osservato/previsto sugli ultimi 20.000 esempi. QUANTO VALE, MISURATO. Valutazione prequenziale sui 3000 dossier piu' recenti (ogni asta prima prevista poi appresa, come nell'uso): sollevamento 10,8x nella cima dell'1%, 5,8x al 5%; calibrazione entro il 10-30% in ogni fascia. Sulle 116 puntate vere dell'utente di agosto: nessuna era finale, e il cancello ne avrebbe fermate 45 senza perderne una vincente. La valutazione congelata (addestra sui vecchi, giudica sui nuovi) sovrastima di 1,5-1,8x per deriva del mercato, non per passo o regolarizzazione: provati entrambi. Griglia e prequenziale stanno nel test MlModelBacktest, target Apprendimento, attivita' VS Code "valuta apprendimento". PRIMO ADDESTRAMENTO. In sottofondo, dal piu' recente, con un tempo massimo per avvio. Il lettore salta le righe di poll prima del JSON: 6289 dossier per 8,8 GB in 64 secondi, verificato dal vivo, archivio completo al primo avvio. ACQUISIZIONE. I poll nel dossier si scrivono solo quando cambia qualcosa di visibile, e comunque uno al secondo: -88,8% misurato, senza perdere la profondita' di nessun ciclo. Il reset porta il minimo del cronometro e la durata del ciclo chiuso. Via le righe di log che copiavano i reset parola per parola. Corretto un difetto vecchio: nessun dossier marcava le puntate nostre, perche' il parser lo faceva solo se riceveva il nome utente e nel percorso dei poll non gli arrivava. Ora si marcano in MergeBidHistory e l'intestazione porta il nome utente; per i dossier vecchi il lettore accetta il nome da fuori. Trovato dal vivo e corretto: salvataggi concorrenti all'avvio quando piu' aste chiudono insieme. Il modello salvato non rilegge piu' i parametri (passo, regolarizzazione): sono scelte del codice, e un cambio vale dal riavvio dopo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
989 lines
50 KiB
C#
989 lines
50 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Text.Json;
|
||
using System.Threading;
|
||
|
||
namespace AutoBidder.Utilities
|
||
{
|
||
public class AppSettings
|
||
{
|
||
/// <summary>
|
||
/// Millisecondi di anticipo sulla scadenza con cui parte la puntata.
|
||
///
|
||
/// <para><b>È il parametro che decide quanto costa vincere</b>, e i dossier
|
||
/// raccolti dicono esattamente dove sta l'ottimo. Il motore punta solo nei cicli
|
||
/// che arrivano fino a questo anticipo senza che nessun altro abbia puntato:
|
||
/// più l'anticipo è largo, più cicli lo raggiungono, più puntate si spendono.</para>
|
||
///
|
||
/// <para>Rigiocando 2.095 dossier (1.007.850 cicli) con <c>backtest.ps1</c>, i cicli
|
||
/// che scendono fino a una data soglia sono:</para>
|
||
/// <list type="table">
|
||
/// <item><description>sotto 1 s - 0,6% dei cicli, mediana <b>1 puntata</b> per asta</description></item>
|
||
/// <item><description>sotto 2 s - 5,9%, mediana 15 puntate</description></item>
|
||
/// <item><description>sotto 3 s - 20,4%, mediana 46 puntate</description></item>
|
||
/// <item><description>sotto 4 s - 79,5%, mediana 97 puntate</description></item>
|
||
/// </list>
|
||
///
|
||
/// <para>Il grosso degli avversari punta con circa tre secondi ancora sul cronometro:
|
||
/// il 58,8% dei cicli si ferma esattamente li'. Sotto i due secondi la concorrenza si
|
||
/// dirada, ed e' il motivo per cui l'anticipo va tenuto stretto - allargarlo di un
|
||
/// secondo moltiplica per dieci le puntate spese.</para>
|
||
///
|
||
/// <para>Dal lato opposto c'e' la rete: ping mediano 56 ms, p99 135 ms, p99,9
|
||
/// 444 ms, con punte oltre i due secondi. Sotto i 600 ms di anticipo basta un picco
|
||
/// perche' la puntata arrivi a giochi chiusi, e una puntata tardiva costa l'asta intera.</para>
|
||
///
|
||
/// <para>Predefinito <b>1000 ms</b>: il valore piu' largo che resta nella fascia da
|
||
/// una puntata per asta, e piu' del doppio del p99,9 del ping. Sotto il secondo i
|
||
/// dossier non possono dire di piu', perche' Bidoo dichiara la scadenza al secondo
|
||
/// intero.</para>
|
||
/// </summary>
|
||
///
|
||
/// <para><b>Aggiornato a 500 ms.</b> Le due ragioni per stare larghi sono cadute
|
||
/// entrambe. La prima — il p99,9 del ping a 444 ms — era misurata quando il tetto di
|
||
/// 40 richieste al secondo faceva accodare le chiamate: con il tetto tolto il ping
|
||
/// misurato sui dossier nuovi sta fra 47 e 78 ms, mediana 55. La seconda era la
|
||
/// paura della concorrenza fitta sotto i due secondi, ma i tipi di puntata dicono
|
||
/// il contrario: il 75,3% delle puntate avversarie è automatica e scatta a 2 s
|
||
/// esatti, quindi sotto quella soglia resta solo chi punta a mano.</para>
|
||
///
|
||
/// <para>A 500 ms la puntata parte con circa nove volte il ping di margine, e cade
|
||
/// nella metà di secondo in cui l'autopuntata del sito ha già sparato. Più in là non
|
||
/// conviene andare: Bidoo dichiara la scadenza al secondo intero, quindi sotto i
|
||
/// 500 ms si starebbe scommettendo sull'arrotondamento.</para>
|
||
/// </summary>
|
||
public int DefaultBidBeforeDeadlineMs { get; set; } = 500;
|
||
public double DefaultMinPrice { get; set; } = 0;
|
||
public double DefaultMaxPrice { get; set; } = 0;
|
||
public int DefaultMaxClicks { get; set; } = 0;
|
||
public int DefaultMinResets { get; set; } = 0;
|
||
public int DefaultMaxResets { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Mostra avviso quando una puntata arriva troppo tardi (timer scaduto).
|
||
/// Suggerisce all'utente di aumentare il tempo di puntata.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool ShowLateBidWarning { get; set; } = true;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// MOTORE DI PRECISIONE — CADENZA POLLING ADATTIVA
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// Ogni asta ha il proprio anello di polling, che si stringe man mano che
|
||
// la scadenza si avvicina: un'asta a otto minuti non ha bisogno di quattro
|
||
// chiamate al secondo, una a tre secondi sì.
|
||
|
||
/// <summary>Cadenza polling oltre i 60 s dalla scadenza. Default: 2000 ms.</summary>
|
||
public int PollIntervalFarMs { get; set; } = 2000;
|
||
|
||
/// <summary>Cadenza polling tra 10 e 60 s dalla scadenza. Default: 900 ms.</summary>
|
||
public int PollIntervalMidMs { get; set; } = 900;
|
||
|
||
/// <summary>Cadenza polling sotto i 10 s dalla scadenza. Default: 400 ms.</summary>
|
||
public int PollIntervalNearMs { get; set; } = 400;
|
||
|
||
/// <summary>
|
||
/// Cadenza polling nella finestra critica, solo per le aste in stato Attiva.
|
||
/// In sola osservazione non c'è puntata da azzeccare e si risparmiano chiamate.
|
||
/// Default: 220 ms.
|
||
/// </summary>
|
||
public int PollIntervalCriticalMs { get; set; } = 220;
|
||
|
||
/// <summary>Ampiezza della finestra critica prima della scadenza. Default: 4000 ms.</summary>
|
||
public int CriticalWindowMs { get; set; } = 4000;
|
||
|
||
/// <summary>
|
||
/// Tetto complessivo di richieste al secondo verso Bidoo (le puntate non sono soggette
|
||
/// al limite: hanno corsia preferenziale). Default: 40.
|
||
/// </summary>
|
||
/// <summary>
|
||
/// Tetto di richieste al secondo verso Bidoo. <b>0 = nessun tetto</b>, ed è il
|
||
/// predefinito: il ritmo lo detta la rete, e a frenare è il server con 429/503.
|
||
///
|
||
/// <para>Il vecchio predefinito era 40, ed era il vero collo di bottiglia. Misurato
|
||
/// sui registri di tre giorni: con fino a 31 aste seguite insieme la distribuzione
|
||
/// delle richieste era tagliata netta sul tetto (p90 39, p99 41), e ogni asta
|
||
/// veniva interrogata ogni ~596 ms invece dei 220 configurati per la finestra
|
||
/// critica. Con il timer a un secondo restava un solo sguardo prima della
|
||
/// scadenza: da lì le puntate perse e il tempo che non scorreva.</para>
|
||
/// </summary>
|
||
public double MaxRequestsPerSecond { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Porta la granularità del timer di sistema a 1 ms mentre il monitor è in funzione.
|
||
/// Senza, ogni attesa può sforare di ~15 ms. Default: true.
|
||
/// </summary>
|
||
public bool PrecisionTimerEnabled { get; set; } = true;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// ASTE PROGRAMMATE — attesa dell'inizio
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// Un'asta che comincia fra tre ore non va interrogata ogni due secondi, ma
|
||
// all'apertura il motore dev'essere già pronto. La cadenza si allarga in
|
||
// proporzione all'attesa e si stringe man mano che l'inizio si avvicina.
|
||
|
||
/// <summary>Attiva la cadenza rilassata per le aste non ancora cominciate.</summary>
|
||
public bool ScheduledAuctionBackoffEnabled { get; set; } = true;
|
||
|
||
/// <summary>Cadenza quando l'inizio è oltre un'ora. Default: 20 minuti.</summary>
|
||
public int ScheduledPollFarSeconds { get; set; } = 1200;
|
||
|
||
/// <summary>Cadenza quando l'inizio è fra 10 e 60 minuti. Default: 2 minuti.</summary>
|
||
public int ScheduledPollMidSeconds { get; set; } = 120;
|
||
|
||
/// <summary>
|
||
/// Quanto prima dell'inizio si torna alla cadenza normale. Default: 120 s —
|
||
/// un margine ampio garantisce di non perdere l'apertura.
|
||
/// </summary>
|
||
public int ScheduledPollWakeSeconds { get; set; } = 120;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// ANTICIPO PUNTATA — misura e taratura
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>
|
||
/// Registra l'anticipo effettivo di ogni puntata, per capire se il valore
|
||
/// impostato è quello giusto. Default: true — è solo una misura, non cambia nulla.
|
||
/// </summary>
|
||
public bool BidLeadTrackingEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Propone (non applica) una correzione dell'anticipo quando i dati raccolti
|
||
/// mostrano uno scarto sistematico. Default: true.
|
||
/// </summary>
|
||
public bool BidLeadSuggestionsEnabled { get; set; } = true;
|
||
|
||
/// <summary>Misure necessarie prima di azzardare una proposta. Default: 15.</summary>
|
||
public int BidLeadMinSamples { get; set; } = 15;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// NOTIFICHE
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>Mostra una notifica di Windows quando un'asta viene vinta.</summary>
|
||
public bool NotifyOnWin { get; set; } = true;
|
||
|
||
/// <summary>Notifica anche quando un'asta seguita viene persa. Default: false.</summary>
|
||
public bool NotifyOnLoss { get; set; } = false;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// CARTELLE DEI DATI
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// Vuoto = posizione predefinita sotto %LocalAppData%\AutoBidder.
|
||
|
||
/// <summary>Dati d'esercizio: aste monitorate e prodotti seguiti.</summary>
|
||
public string DataFolder { get; set; } = "";
|
||
|
||
/// <summary>
|
||
/// Statistiche ed esportazioni. Separata perché cresce nel tempo e serve
|
||
/// soprattutto per le analisi fatte fuori dall'applicazione.
|
||
/// </summary>
|
||
public string StatsFolder { get; set; } = "";
|
||
|
||
/// <summary>
|
||
/// Registri di testo: log applicativo, riscatto puntate, dossier delle aste.
|
||
/// Vuoto = <c>Dati\Registri</c>.
|
||
/// </summary>
|
||
public string LogFolder { get; set; } = "";
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// REGISTRI SU FILE
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>Scrive il log applicativo anche su file, un file per giorno.</summary>
|
||
public bool WriteAppLogFile { get; set; } = true;
|
||
|
||
/// <summary>Scrive su file il registro del riscatto puntate.</summary>
|
||
public bool WriteFreeBidsLogFile { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Registra un dossier per ogni asta seguita: intestazione, eventi e riepilogo, in
|
||
/// formato JSON Lines. È il materiale su cui si studiano le strategie, e va raccolto
|
||
/// mentre l'asta è in corso — a posteriori Bidoo non espone nulla di tutto questo.
|
||
/// </summary>
|
||
public bool WriteAuctionDossiers { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Registra nel dossier <b>ogni singola interrogazione</b> a Bidoo, non solo gli
|
||
/// eventi. È la risoluzione massima — prezzo, timer e ping a ogni poll — e il prezzo
|
||
/// da pagare è la dimensione: nella finestra critica sono quattro righe al secondo,
|
||
/// quindi un'asta lunga vale qualche megabyte.
|
||
/// </summary>
|
||
public bool DossierIncludeRawPolls { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Dopo quanti giorni cancellare i registri. 0 = tenerli per sempre.
|
||
/// Predefinito 90: con i poll grezzi accesi lo spazio va tenuto d'occhio.
|
||
/// </summary>
|
||
public int LogRetentionDays { get; set; } = 90;
|
||
|
||
/// <summary>
|
||
/// Registra una scheda dettagliata per ogni asta conclusa (serie dei prezzi,
|
||
/// puntate per utente, durata). È la materia prima per migliorare le strategie.
|
||
/// </summary>
|
||
public bool DetailedStatsEnabled { get; set; } = true;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// CATALOGO — cache
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>
|
||
/// Per quanti secondi riusare le aste già scaricate di una categoria.
|
||
/// Ogni caricamento costa una richiesta ogni cinquanta aste: senza cache,
|
||
/// tornare avanti e indietro fra due categorie le rifà tutte ogni volta.
|
||
/// Default: 60 s. 0 = nessuna cache.
|
||
/// </summary>
|
||
public int CatalogCacheSeconds { get; set; } = 60;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// ESPLORA — CATALOGO
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>
|
||
/// Quante aste caricare al massimo per categoria. Il listato si sfoglia a pagine
|
||
/// di cinquanta, in ordine di scadenza: il tetto taglia via le aste più lontane,
|
||
/// mai quelle imminenti. Le categorie piccole finiscono prima e si fermano da sole.
|
||
/// Default: 500 (dieci pagine).
|
||
/// </summary>
|
||
public int CatalogMaxAuctions { get; set; } = 500;
|
||
|
||
/// <summary>
|
||
/// Aggiorna prezzi e timer delle schede mentre le guardi.
|
||
/// Default: false — con l'aggiornamento acceso la pagina cambia sotto gli occhi
|
||
/// mentre si legge, quindi va acceso solo quando serve davvero.
|
||
/// </summary>
|
||
public bool CatalogAutoRefresh { get; set; } = false;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// AGGIUNTA AUTOMATICA DEI PRODOTTI SEGUITI
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// Bidoo rimette all'asta lo stesso articolo di continuo: seguendo il prodotto
|
||
// (stellina in Esplora) le aste nuove entrano nel monitor da sole.
|
||
|
||
/// <summary>Attiva la scansione periodica del catalogo per i prodotti seguiti.</summary>
|
||
public bool AutoAddProductsEnabled { get; set; } = true;
|
||
|
||
/// <summary>Ogni quanti secondi cercare aste nuove dei prodotti seguiti. Default: 90.</summary>
|
||
public int AutoAddScanSeconds { get; set; } = 90;
|
||
|
||
/// <summary>
|
||
/// Quante aste percorrere nella scansione dei prodotti seguiti.
|
||
///
|
||
/// <para>È un valore <b>a parte</b> da quello del catalogo, e più alto, per una
|
||
/// ragione precisa: il listato è ordinato per scadenza, quindi le aste che devono
|
||
/// ancora cominciare stanno in fondo. Con il tetto del catalogo (500) un'asta che
|
||
/// apre fra tre ore resta invisibile finché non si avvicina — ed è esattamente
|
||
/// quella che si vorrebbe avere nel monitor <i>prima</i> che parta.</para>
|
||
///
|
||
/// <para>Predefinito 2500: una cinquantina di richieste per scansione, che con la
|
||
/// cadenza predefinita sono meno di una al secondo.</para>
|
||
/// </summary>
|
||
public int AutoAddScanMaxAuctions { get; set; } = 2500;
|
||
|
||
/// <summary>
|
||
/// Stato con cui entrano le aste aggiunte automaticamente.
|
||
/// Valori: "Stopped", "Watch" (predefinito), "Active".
|
||
///
|
||
/// Il valore predefinito è deliberatamente <b>Osserva</b>: un'asta che entra da sola
|
||
/// e comincia subito a puntare spenderebbe puntate vere senza che tu l'abbia vista.
|
||
/// </summary>
|
||
public string AutoAddNewAuctionState { get; set; } = "Watch";
|
||
|
||
/// <summary>
|
||
/// Tetto di aste aggiunte automaticamente presenti insieme nel monitor
|
||
/// (0 = nessun limite). Default: 20.
|
||
/// </summary>
|
||
public int AutoAddMaxAuctions { get; set; } = 20;
|
||
|
||
/// <summary>
|
||
/// Orizzonte temporale della sorveglianza: un'asta che comincia fra <b>più</b> di
|
||
/// tanti minuti non entra da sola nel monitor. 0 = nessun limite.
|
||
///
|
||
/// <para>La ricerca scende in fondo al listato apposta per trovare le aste
|
||
/// programmate, ma "programmata" su Bidoo vuol dire anche fra dodici ore: senza un
|
||
/// orizzonte il monitor si riempie di aste che non succederanno per mezza giornata,
|
||
/// consumando il tetto di <see cref="AutoAddMaxAuctions"/> e nascondendo quelle
|
||
/// vicine. A mano si può sempre aggiungere qualunque asta: questo limite vale solo
|
||
/// per l'aggiunta automatica.</para>
|
||
///
|
||
/// <para>Predefinito 60 minuti. Un'asta in corso ha per costruzione un timer di
|
||
/// pochi secondi, quindi non è mai toccata dal filtro.</para>
|
||
/// </summary>
|
||
public int AutoAddMaxStartMinutes { get; set; } = 60;
|
||
|
||
/// <summary>
|
||
/// Aggiungi da solo <b>solo</b> le aste che non sono ancora cominciate, cioè quelle
|
||
/// su cui nessuno ha ancora puntato.
|
||
///
|
||
/// <para>Un'asta presa a metà si può seguire, ma la sua storia è già scritta per
|
||
/// buona parte: non si sa quanto è durata davvero, chi c'era prima, come è salito il
|
||
/// prezzo. Le statistiche che ne escono sono monche, e i dossier — che sono il
|
||
/// materiale su cui si tarano le strategie — raccontano solo la seconda metà.
|
||
/// Accendendo questa opzione entrano nel monitor soltanto le aste che si potranno
|
||
/// seguire dal primo istante all'ultimo.</para>
|
||
///
|
||
/// <para>Il prezzo da pagare è che si perdono le occasioni già in corso. Predefinito
|
||
/// <c>false</c>: chi vuole puntare, e non solo misurare, di solito le vuole entrambe.</para>
|
||
/// </summary>
|
||
public bool AutoAddOnlyNotStarted { get; set; } = false;
|
||
|
||
// RISCATTO AUTOMATICO DELLE PUNTATE GRATIS
|
||
// Bidoo lascia le ricompense dei giorni scorsi in attesa sulla pagina dedicata:
|
||
// un controllo periodico le raccoglie senza doverci pensare.
|
||
|
||
/// <summary>Attiva il controllo periodico delle ricompense da riscuotere.</summary>
|
||
public bool FreeBidsAutoClaimEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Ogni quanti minuti controllare. Il minimo applicato è 5: le ricompense sono
|
||
/// giornaliere, e interrogare il sito più spesso sarebbe traffico sprecato.
|
||
/// Default: 30.
|
||
/// </summary>
|
||
public int FreeBidsCheckMinutes { get; set; } = 30;
|
||
|
||
// LIMITI LOG
|
||
/// <summary>
|
||
/// Numero massimo di righe di log da mantenere per ogni singola asta (default: 500)
|
||
/// </summary>
|
||
public int MaxLogLinesPerAuction { get; set; } = 500;
|
||
|
||
/// <summary>
|
||
/// Numero massimo di righe di log da mantenere nel log globale (default: 1000)
|
||
/// </summary>
|
||
public int MaxGlobalLogLines { get; set; } = 1000;
|
||
|
||
// NUOVE IMPOSTAZIONI STATO INIZIALE ASTE
|
||
/// <summary>
|
||
/// Determina se all'apertura dell'applicazione le aste salvate devono essere avviate automaticamente.
|
||
/// Valori: "Active" = avvia, "Paused" = in pausa, "Stopped" = fermate (default)
|
||
/// </summary>
|
||
public string DefaultStartAuctionsOnLoad { get; set; } = "Stopped";
|
||
|
||
/// <summary>
|
||
/// Determina lo stato iniziale di una nuova asta quando viene aggiunta.
|
||
/// Valori: "Active" = attiva, "Paused" = in pausa, "Stopped" = fermata (default)
|
||
/// </summary>
|
||
public string DefaultNewAuctionState { get; set; } = "Stopped";
|
||
|
||
/// <summary>
|
||
/// Se TRUE, salva e ripristina lo stato effettivo (attiva/pausa/ferma) di ogni asta.
|
||
/// Se FALSE, applica DefaultStartAuctionsOnLoad a tutte le aste al caricamento.
|
||
/// Default: false (usa DefaultStartAuctionsOnLoad)
|
||
/// </summary>
|
||
public bool RememberAuctionStates { get; set; } = false;
|
||
|
||
// ? NUOVO: LIMITE MINIMO PUNTATE
|
||
/// <summary>
|
||
/// Numero minimo di puntate residue da mantenere sull'account.
|
||
/// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.
|
||
/// Default: 0 (nessun limite)
|
||
/// </summary>
|
||
public int MinimumRemainingBids { get; set; } = 0;
|
||
|
||
// ?? NUOVO: LIMITE STORIA PUNTATE
|
||
/// <summary>
|
||
/// Numero massimo di puntate da visualizzare nella scheda "Storia Puntate".
|
||
/// Se impostato > 0, mostra solo le ultime N puntate dall'API.
|
||
/// Default: 20 (ultime 20 puntate)
|
||
/// </summary>
|
||
public int MaxBidHistoryEntries { get; set; } = 20;
|
||
|
||
// ?? NUOVO: LIVELLO MINIMO LOG
|
||
/// <summary>
|
||
/// Livello minimo di log da visualizzare.
|
||
/// Valori: "ErrorOnly" (solo errori), "Normal" (errori e warning),
|
||
/// "Informational" (info standard), "Debug" (dettagli sviluppo), "Trace" (tutto).
|
||
/// Default: "Normal" (uso giornaliero - errori e warning)
|
||
/// </summary>
|
||
public string MinLogLevel { get; set; } = "Normal";
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// IMPOSTAZIONI DATABASE
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Abilita il salvataggio automatico delle aste completate nel database.
|
||
/// Default: true (consigliato per statistiche)
|
||
/// </summary>
|
||
public bool DatabaseAutoSaveEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Esegue pulizia automatica duplicati all'avvio dell'applicazione.
|
||
/// Default: true (consigliato per mantenere database pulito)
|
||
/// </summary>
|
||
public bool DatabaseAutoCleanupDuplicates { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Esegue pulizia automatica record incompleti all'avvio.
|
||
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
|
||
/// </summary>
|
||
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Numero massimo di giorni da mantenere nei risultati aste.
|
||
/// Record più vecchi vengono eliminati automaticamente.
|
||
/// Default: 180 (6 mesi), 0 = disabilitato
|
||
/// </summary>
|
||
public int DatabaseMaxRetentionDays { get; set; } = 180;
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// STRATEGIE AVANZATE DI PUNTATA
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
// ❌ RIMOSSO: Jitter, Offset Dinamico, Latenza Adattiva
|
||
// Il timing è gestito SOLO da DefaultBidBeforeDeadlineMs
|
||
// Le strategie decidono SE puntare, non QUANDO
|
||
|
||
// 🎯 LOGGING GRANULARE
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Log quando viene piazzata una puntata [BID]
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool LogBids { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Log quando una strategia blocca la puntata [STRATEGY]
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool LogStrategyDecisions { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Log calcoli valore prodotto [VALUE]
|
||
/// Default: false (attiva per debug)
|
||
/// </summary>
|
||
public bool LogValueCalculations { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Log rilevamento competizione e heat [COMPETITION]
|
||
/// Default: false
|
||
/// </summary>
|
||
public bool LogCompetition { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Log timing e polling (molto verbose!) [TIMING]
|
||
/// Default: false (attiva solo per debug timing)
|
||
/// </summary>
|
||
public bool LogTiming { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Log errori e warning [ERROR/WARN]
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool LogErrors { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Applica automaticamente i limiti salvati nel prodotto quando si aggiunge una nuova asta.
|
||
/// Se TRUE e il prodotto ha valori di default salvati, li applica automaticamente.
|
||
/// Default: true (consigliato per coerenza)
|
||
/// </summary>
|
||
public bool AutoApplyProductDefaults { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Scelta priorità limiti quando si aggiunge un'asta per un prodotto già salvato:
|
||
/// - "ProductStats": Usa i limiti personalizzati salvati nelle statistiche prodotto (UserDefaultMinPrice, ecc.)
|
||
/// - "GlobalDefaults": Usa sempre i limiti globali (DefaultMinPrice, DefaultMaxPrice, ecc.)
|
||
/// Default: "ProductStats" (consigliato per usare limiti specifici per prodotto)
|
||
/// </summary>
|
||
public string NewAuctionLimitsPriority { get; set; } = "ProductStats";
|
||
|
||
/// <summary>
|
||
/// Log stato asta (terminata, reset, ecc.) [STATUS]
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool LogAuctionStatus { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Log profiling avversari [OPPONENT]
|
||
/// Default: false
|
||
/// </summary>
|
||
public bool LogOpponentProfiling { get; set; } = false;
|
||
|
||
// 🎯 STRATEGIE SEMPLIFICATE
|
||
|
||
|
||
/// <summary>
|
||
/// Entry Point: Usato SOLO per calcolare i limiti consigliati (70% del MaxPrice storico).
|
||
/// NON blocca le puntate! I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
||
/// Default: true (per calcolo limiti consigliati)
|
||
/// </summary>
|
||
public bool EntryPointEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Anti-bot: rinuncia al ciclo quando l'ultimo puntatore punta a cadenza fissa.
|
||
///
|
||
/// <para><b>Predefinito cambiato a false.</b> Le marche temporali di Bidoo sono in
|
||
/// secondi interi, quindi la regola non puo' davvero distinguere un automatismo da
|
||
/// una persona regolare: si riduce a "le ultime pause dell'avversario sono identiche
|
||
/// al secondo", cosa comunissima. Rigiocando i dossier con <c>backtest.ps1</c>
|
||
/// rifiutava fra il <b>4% e il 9%</b> delle puntate, a seconda dell'anticipo, e la
|
||
/// premessa era comunque rovesciata: un avversario a cadenza fissa punta con secondi
|
||
/// di anticipo, quindi e' il piu' facile da battere aspettando l'ultimo istante.</para>
|
||
///
|
||
/// <para>Per rivedere il confronto con i propri dati:
|
||
/// <c>$env:AUTOBIDDER_BACKTEST_LEGACY=1</c> prima di <c>backtest.ps1</c>.</para>
|
||
/// </summary>
|
||
public bool AntiBotDetectionEnabled { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// User Exhaustion: Sfrutta utenti stanchi (oltre 50 puntate)
|
||
/// quando ci sono pochi altri bidder attivi.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool UserExhaustionEnabled { get; set; } = true;
|
||
|
||
// 🎯 CONTROLLO CONVENIENZA PRODOTTO
|
||
|
||
/// <summary>
|
||
/// Abilita il controllo di convenienza basato sul valore del prodotto.
|
||
/// Se attivo, blocca le puntate quando il costo totale supera il prezzo "Compra Subito"
|
||
/// di una percentuale superiore a MinSavingsPercentage.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool ValueCheckEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Percentuale minima di risparmio richiesta per continuare a puntare.
|
||
/// Valori negativi = tolleranza alla perdita.
|
||
/// Es: -5 = permetti fino al 5% di perdita rispetto al "Compra Subito"
|
||
/// 0 = blocca se costa uguale o più del "Compra Subito"
|
||
/// 10 = richiedi almeno 10% di risparmio
|
||
/// Default: -5 (permetti fino al 5% di perdita)
|
||
/// </summary>
|
||
public double MinSavingsPercentage { get; set; } = -5.0;
|
||
|
||
/// <summary>
|
||
/// Abilita il controllo anti-collisione hardcoded.
|
||
/// Se attivo, blocca le puntate quando ci sono 3+ bidder attivi negli ultimi 10 secondi.
|
||
/// ATTENZIONE: Questo controllo può far perdere aste competitive!
|
||
/// Default: false (DISABILITATO - non blocca mai)
|
||
/// </summary>
|
||
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
|
||
|
||
// ── Fascia oraria sospesa ────────────────────────────────────────
|
||
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
|
||
// doppio che fra le 10 e le 13.
|
||
|
||
// ── Apprendimento ────────────────────────────────────────────────
|
||
// Vedi Ml/LearningService. Sempre acceso: impara da ogni asta chiusa.
|
||
|
||
/// <summary>
|
||
/// Lasciar fermare una puntata al modello quando il valore atteso è negativo.
|
||
/// Spento, il modello parla nel registro ma non decide.
|
||
/// </summary>
|
||
public bool LearningGateEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Aste apprese sotto le quali il modello non ferma niente. Un modello appena nato
|
||
/// direbbe cose a caso, e a caso fermerebbe le puntate. Predefinito 300.
|
||
/// </summary>
|
||
public int LearningMinAuctions { get; set; } = 300;
|
||
|
||
/// <summary>
|
||
/// Moltiplica il costo della puntata nel confronto col valore atteso: 1 è il
|
||
/// valore atteso puro, 2 pretende che una puntata renda il doppio del suo costo.
|
||
/// Più alto = più selettivo = meno puntate.
|
||
/// </summary>
|
||
public double LearningEvMultiplier { get; set; } = 1.0;
|
||
|
||
/// <summary>
|
||
/// Secondi per avvio dedicati a leggere i dossier non ancora appresi, in
|
||
/// sottofondo. La prima volta ne servono diversi avvii; poi resta solo l'asta
|
||
/// appena chiusa.
|
||
/// </summary>
|
||
public int LearningBootstrapSecondsPerStart { get; set; } = 120;
|
||
|
||
// ── Rimozione automatica delle aste concluse ─────────────────────
|
||
// Vedi FinishedAuctionCleanup: si toglie di serie, si trattiene cio' su cui c'e'
|
||
// stato un esborso.
|
||
|
||
/// <summary>Togliere da sole dall'elenco le aste concluse. Acceso di serie.</summary>
|
||
public bool AutoRemoveFinished { get; set; } = true;
|
||
|
||
/// <summary>Trattenere le aste su cui hai puntato, per controllarle a mano.</summary>
|
||
public bool AutoRemoveKeepWithMyBids { get; set; } = true;
|
||
|
||
/// <summary>Trattenere le aste vinte: c'è da confermare l'acquisto su Bidoo.</summary>
|
||
public bool AutoRemoveKeepWon { get; set; } = true;
|
||
|
||
/// <summary>Trattenere le aste di cui non si è vista la fine: esito da controllare.</summary>
|
||
public bool AutoRemoveKeepUnclear { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Versione dello schema delle impostazioni, per le migrazioni una tantum.
|
||
///
|
||
/// <para>Un file scritto da una versione precedente non ha questo campo e vale
|
||
/// quindi 0. Serve perché un predefinito nuovo <b>non arriva</b> a chi ha già un
|
||
/// file salvato: il valore su disco vince sempre, ed è giusto che sia così, tranne
|
||
/// quando il vecchio valore veniva da una misura che si è poi rivelata sbagliata.
|
||
/// Vedi <see cref="Migra"/>.</para>
|
||
/// </summary>
|
||
public int SettingsSchemaVersion { get; set; }
|
||
|
||
/// <summary>
|
||
/// Sospendere le puntate nella fascia indicata. Acceso di serie.
|
||
/// L'asta resta comunque Attiva e riprende da sola: vedi <see cref="BiddingHours"/>.
|
||
/// </summary>
|
||
public bool QuietHoursEnabled { get; set; } = true;
|
||
|
||
/// <summary>Prima ora della fascia sospesa, inclusa.</summary>
|
||
public int QuietHoursStart { get; set; } = BiddingHours.DefaultStart;
|
||
|
||
/// <summary>Prima ora in cui si torna a puntare.</summary>
|
||
public int QuietHoursEnd { get; set; } = BiddingHours.DefaultEnd;
|
||
|
||
/// <summary>
|
||
/// Ritirarsi quando dall'altra parte c'è un'autopuntata armata. Acceso di serie.
|
||
/// Vedi <see cref="AutoBidDuel"/> per il meccanismo e i numeri misurati.
|
||
/// </summary>
|
||
public bool AutoBidDuelWithdrawEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Risposte automatiche di fila oltre le quali ci si ritira. Predefinito 5.
|
||
/// Più basso risparmia prima ma rischia di scambiare per macchina un avversario
|
||
/// umano regolare; più alto è prudente e costa puntate.
|
||
/// </summary>
|
||
public int AutoBidDuelResponses { get; set; } = AutoBidDuel.RispostePerConcludere;
|
||
|
||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||
|
||
/// <summary>
|
||
/// Abilita rilevamento competizione e heat metric.
|
||
/// Conta bidder attivi e collisioni per determinare il "calore" dell'asta.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool CompetitionDetectionEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Finestra temporale in secondi per contare bidder attivi.
|
||
/// Default: 30 (ultimi 30 secondi)
|
||
/// </summary>
|
||
public int CompetitionWindowSeconds { get; set; } = 30;
|
||
|
||
/// <summary>
|
||
/// Numero minimo di bidder attivi per considerare l'asta "affollata".
|
||
/// Se >= a questa soglia, applica logica di evitamento.
|
||
/// Default: 3
|
||
/// </summary>
|
||
public int CompetitionThreshold { get; set; } = 3;
|
||
|
||
/// <summary>
|
||
/// Abilita auto-pausa per aste troppo competitive.
|
||
/// Default: false (solo warning, non pausa automatica)
|
||
/// </summary>
|
||
public bool AutoPauseHotAuctions { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Soglia heat metric per auto-pausa (0-100).
|
||
/// Default: 80 (pausa se heat >= 80%)
|
||
/// </summary>
|
||
public int HeatThresholdForPause { get; set; } = 80;
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// SOFT RETREAT E COLLISION MANAGEMENT
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Abilita soft retreat automatico dopo N collisioni consecutive.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool SoftRetreatEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Numero di collisioni consecutive per attivare soft retreat.
|
||
/// Default: 3
|
||
/// </summary>
|
||
public int SoftRetreatAfterCollisions { get; set; } = 3;
|
||
|
||
/// <summary>
|
||
/// Durata del ritiro, in secondi.
|
||
///
|
||
/// <para>Ridotta da 30 a 12: il timer delle aste osservate si azzera ogni 8-12
|
||
/// secondi, quindi trenta secondi di pausa sono tre cicli interi — nella pratica
|
||
/// l'asta è persa. Dodici secondi bastano a spezzare una serie di collisioni
|
||
/// senza consegnare la partita.</para>
|
||
/// </summary>
|
||
public int SoftRetreatDurationSeconds { get; set; } = 12;
|
||
|
||
/// <summary>
|
||
/// Blocca la puntata se il prezzo sale più in fretta di tanti euro al secondo.
|
||
/// 0 = controllo spento (predefinito).
|
||
///
|
||
/// <para>Prima era una costante nel codice fissata a 0,10 €/s, cioè dieci puntate
|
||
/// al secondo: su oltre 40.000 valutazioni riprese dai dossier non è mai scattata,
|
||
/// e la velocità massima mai osservata è 0,016 €/s. Se lo si vuole usare davvero,
|
||
/// un valore sensato sta intorno a 0,01-0,02 €/s.</para>
|
||
/// </summary>
|
||
public double PriceVelocityBlockPerSecond { get; set; } = 0;
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// PROBABILISTIC BIDDING
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Abilita policy di puntata probabilistica.
|
||
/// Decide se puntare con probabilità p basata su competizione e ROI.
|
||
/// Default: false (richiede tuning)
|
||
/// </summary>
|
||
public bool ProbabilisticBiddingEnabled { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Probabilità base di puntata (0.0 - 1.0).
|
||
/// Default: 0.8 (80%)
|
||
/// </summary>
|
||
public double BaseBidProbability { get; set; } = 0.8;
|
||
|
||
/// <summary>
|
||
/// Fattore di riduzione probabilità per ogni bidder attivo extra.
|
||
/// Default: 0.1 (riduce del 10% per ogni bidder oltre la soglia)
|
||
/// </summary>
|
||
public double ProbabilityReductionPerBidder { get; set; } = 0.1;
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// OPPONENT PROFILING
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Abilita profiling degli avversari.
|
||
/// Identifica utenti aggressivi e applica regole specifiche.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool OpponentProfilingEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Soglia puntate per considerare un utente "aggressivo".
|
||
/// Default: 10 (se un utente ha fatto >= 10 puntate in un'asta)
|
||
/// </summary>
|
||
public int AggressiveBidderThreshold { get; set; } = 10;
|
||
|
||
/// <summary>
|
||
/// Dimensione finestra scorrevole per analisi bidder aggressivi.
|
||
/// Analizza le ultime N puntate invece del conteggio totale.
|
||
/// Default: 30 (ultime 30 puntate)
|
||
/// </summary>
|
||
public int AggressiveBidderWindowSize { get; set; } = 30;
|
||
|
||
/// <summary>
|
||
/// Soglia percentuale per considerare un utente "aggressivo".
|
||
/// Se un utente ha più di X% delle puntate nella finestra, è aggressivo.
|
||
/// Default: 40 (40% delle puntate)
|
||
/// </summary>
|
||
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
|
||
|
||
/// <summary>
|
||
/// Dimensione finestra per rilevamento situazioni di duello.
|
||
/// Default: 20 (ultime 20 puntate)
|
||
/// </summary>
|
||
public int DuelDetectionWindowSize { get; set; } = 20;
|
||
|
||
/// <summary>
|
||
/// Azione da intraprendere con bidder aggressivi.
|
||
/// "Avoid" = evita l'asta, "Compete" = continua normalmente, "Outbid" = punta più aggressivamente
|
||
/// Default: "Compete" (cambiato da Avoid per essere meno restrittivo)
|
||
/// </summary>
|
||
public string AggressiveBidderAction { get; set; } = "Compete";
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// BANKROLL & SAFETY MANAGER
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Abilita gestione bankroll per limitare spese.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool BankrollManagerEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Limite massimo puntate per sessione (0 = illimitato).
|
||
/// Default: 0
|
||
/// </summary>
|
||
public int MaxBidsPerSession { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Limite massimo puntate per singola asta (0 = illimitato).
|
||
/// Default: 0
|
||
/// </summary>
|
||
public int MaxBidsPerAuction { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Budget massimo giornaliero in euro (0 = illimitato).
|
||
/// Calcolato come: puntate usate × costo medio puntata.
|
||
/// Default: 0
|
||
/// </summary>
|
||
public double DailyBudgetEuro { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Costo medio di una puntata, in euro. Entra nel budget e nel tetto di prezzo
|
||
/// consigliato per ogni prodotto. Predefinito 0,20: è il valore che il motore usa
|
||
/// nei calcoli di convenienza durante l'asta, e averne due diversi voleva dire
|
||
/// vedere due conti diversi sullo stesso articolo.
|
||
/// </summary>
|
||
public double AverageBidCostEuro { get; set; } = 0.20;
|
||
|
||
/// <summary>
|
||
/// Quota delle chiusure passate che il prezzo massimo consigliato punta a coprire.
|
||
///
|
||
/// <para>Alzarla vuol dire restare in gioco in più aste ma spingersi su prezzi più
|
||
/// alti; abbassarla vuol dire puntare solo sulle occasioni buone e lasciar perdere
|
||
/// le altre. Predefinito <b>85%</b>: la sola moda — il prezzo a cui l'articolo si
|
||
/// aggiudica più spesso — coprirebbe fra il 15% e il 44% delle aste a seconda del
|
||
/// prodotto, cioè si resterebbe fuori dalla maggior parte delle occasioni.</para>
|
||
///
|
||
/// <para>Il consiglio resta comunque limitato dal budget: vedi
|
||
/// <see cref="PriceAdvisor"/>.</para>
|
||
/// </summary>
|
||
public double SuggestedPriceCoveragePercent { get; set; } = 85;
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// LOGGING AVANZATO
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Abilita logging avanzato con metriche dettagliate.
|
||
/// Include: collisioni, timer scaduto, latenza, heat metric.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool AdvancedLoggingEnabled { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Salva metriche per ogni puntata nel database.
|
||
/// Default: true
|
||
/// </summary>
|
||
public bool SaveBidMetricsToDatabase { get; set; } = true;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// INTERFACCIA
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>
|
||
/// Abilita la modalità scura dell'interfaccia.
|
||
/// Default: true (modalità scura)
|
||
/// </summary>
|
||
public bool DarkMode { get; set; } = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Impostazioni su file, con una copia in memoria.
|
||
///
|
||
/// <see cref="Load"/> è su un percorso caldissimo: ogni anello di polling e ogni giro
|
||
/// del cecchino la chiamano, quindi decine di volte al secondo per ogni asta seguita.
|
||
/// Per questo la lettura del caso comune è <b>senza lucchetto</b>: si legge un
|
||
/// riferimento volatile, che in .NET è atomico. Il lucchetto serve solo a chi ricarica
|
||
/// da disco, cioè una volta ogni due secondi al massimo.
|
||
/// </summary>
|
||
public static class SettingsManager
|
||
{
|
||
// Le impostazioni stanno nella radice fissa: contengono il percorso degli altri
|
||
// dati, quindi non possono a loro volta dipendere da quel percorso.
|
||
private static string _folder => AppPaths.ConfigRoot;
|
||
private static string _file => AppPaths.SettingsFile;
|
||
|
||
private static readonly object _reloadLock = new();
|
||
private static volatile AppSettings? _cached;
|
||
|
||
/// <summary>Scadenza della copia in memoria, in tick UTC.</summary>
|
||
private static long _cacheExpiryTicks;
|
||
|
||
private const int CACHE_TTL_MS = 2000;
|
||
|
||
/// <summary>Schema corrente. Alzarlo fa girare <see cref="Migra"/> una volta sola.</summary>
|
||
public const int SchemaCorrente = 1;
|
||
|
||
/// <summary>
|
||
/// Porta avanti un file scritto da una versione precedente. Restituisce true se
|
||
/// qualcosa è cambiato, così chi chiama sa che va risalvato.
|
||
///
|
||
/// <para><b>Perché serve.</b> Il valore su disco vince sui predefiniti, ed è
|
||
/// giusto: sono scelte dell'utente. Ma due di quei valori non erano scelte, erano
|
||
/// conseguenze di misure sbagliate — il tetto di 40 richieste al secondo e
|
||
/// l'anticipo di 1000 ms erano stati tarati quando il tetto stesso accodava le
|
||
/// chiamate e gonfiava il ping a 444 ms. Senza migrazione, chi aggiorna si ritrova
|
||
/// le correzioni di oggi già annullate dal proprio file, e conclude che non
|
||
/// funzionano.</para>
|
||
///
|
||
/// <para>Si tocca solo chi ha ancora esattamente il vecchio valore. Chi lo aveva
|
||
/// già cambiato di suo se lo tiene: quella è una scelta, e non spetta a un
|
||
/// aggiornamento ribaltarla.</para>
|
||
/// </summary>
|
||
public static bool Migra(AppSettings s)
|
||
{
|
||
if (s == null || s.SettingsSchemaVersion >= SchemaCorrente) return false;
|
||
|
||
if (Math.Abs(s.MaxRequestsPerSecond - 40) < 0.001) s.MaxRequestsPerSecond = 0;
|
||
if (s.DefaultBidBeforeDeadlineMs == 1000) s.DefaultBidBeforeDeadlineMs = 500;
|
||
|
||
s.SettingsSchemaVersion = SchemaCorrente;
|
||
return true;
|
||
}
|
||
|
||
public static AppSettings Load()
|
||
{
|
||
// Percorso comune: nessun lucchetto, nessuna allocazione.
|
||
var cached = _cached;
|
||
if (cached != null && DateTime.UtcNow.Ticks < Interlocked.Read(ref _cacheExpiryTicks))
|
||
return cached;
|
||
|
||
lock (_reloadLock)
|
||
{
|
||
// Un altro thread potrebbe aver già ricaricato mentre aspettavamo.
|
||
cached = _cached;
|
||
if (cached != null && DateTime.UtcNow.Ticks < Interlocked.Read(ref _cacheExpiryTicks))
|
||
return cached;
|
||
|
||
AppSettings loaded;
|
||
var daRisalvare = false;
|
||
|
||
try
|
||
{
|
||
var esiste = File.Exists(_file);
|
||
|
||
loaded = esiste
|
||
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(_file)) ?? new AppSettings()
|
||
: new AppSettings();
|
||
|
||
// Un file nuovo nasce già allo schema corrente: la migrazione riguarda
|
||
// solo chi arriva da una versione precedente.
|
||
if (!esiste) loaded.SettingsSchemaVersion = SchemaCorrente;
|
||
else daRisalvare = Migra(loaded);
|
||
}
|
||
catch
|
||
{
|
||
// File illeggibile o corrotto: si tiene quello che c'era, o i predefiniti.
|
||
loaded = _cached ?? new AppSettings();
|
||
}
|
||
|
||
// Scritto qui e non dentro Save: Load è sul percorso caldo e non deve
|
||
// toccare il disco, ma una migrazione capita una volta sola nella vita
|
||
// del file, e lasciarla non salvata la farebbe ripetere a ogni avvio.
|
||
if (daRisalvare)
|
||
{
|
||
try { File.WriteAllText(_file, JsonSerializer.Serialize(loaded, new JsonSerializerOptions { WriteIndented = true })); }
|
||
catch { /* si riproverà al prossimo avvio */ }
|
||
}
|
||
|
||
_cached = loaded;
|
||
Interlocked.Exchange(ref _cacheExpiryTicks, DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS).Ticks);
|
||
return loaded;
|
||
}
|
||
}
|
||
|
||
public static void Save(AppSettings settings)
|
||
{
|
||
try
|
||
{
|
||
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
|
||
var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
|
||
File.WriteAllText(_file, txt);
|
||
|
||
// Pubblica subito i nuovi valori: il motore li vede al giro successivo.
|
||
_cached = settings;
|
||
Interlocked.Exchange(ref _cacheExpiryTicks, DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS).Ticks);
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
}
|