Files
Mimante/Mimante/Data/AuctionDatabase.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

329 lines
14 KiB
C#

using System;
using System.Threading;
using AutoBidder.Utilities;
namespace AutoBidder.Data
{
/// <summary>
/// L'archivio delle osservazioni: un solo file SQLite con tutto ciò che si osserva e
/// si decide — aste, puntate, interrogazioni, reset, nostre puntate, decisioni del
/// motore, misure di rete, puntatori, sessioni, contabilità del rischio.
///
/// <para><b>Perché un database.</b> Prima c'erano un file JSON per asta (i dossier),
/// un file JSON con lo storico compatto, uno con le statistiche per prodotto: copie
/// parziali degli stessi fatti, da tenere allineate, e nessuna interrogabile. Qui ogni
/// fatto sta in una riga sola, con un indice, e una domanda come «quante puntate
/// manuali fra le 10 e le 13 sui buoni da 10 € sono rimaste senza risposta?» è una
/// riga di SQL invece di una lettura di otto gigabyte.</para>
///
/// <para>Il motore (coda di scrittura, letture in WAL) è in <see cref="SqliteDatabase"/>.
/// I dati d'esercizio e i registri stanno nell'altro file, <see cref="OperationalDatabase"/>.</para>
/// </summary>
public sealed class AuctionDatabase : SqliteDatabase
{
public const int SchemaVersion = 1;
private static readonly object InstanceSync = new();
private static AuctionDatabase? _instance;
/// <summary>L'archivio dell'applicazione, aperto al primo uso in <see cref="AppPaths.DatabaseFile"/>.</summary>
public static AuctionDatabase Instance
{
get
{
lock (InstanceSync)
{
if (_instance == null || _instance.IsDisposed)
_instance = Open(AppPaths.DatabaseFile);
return _instance;
}
}
}
/// <summary>Chiude l'archivio dell'applicazione svuotando la coda. All'uscita, o prima di spostarlo.</summary>
public static void CloseInstance()
{
lock (InstanceSync)
{
_instance?.Dispose();
_instance = null;
}
}
private long _nextDecisionId;
private long _auctionsVersion;
/// <summary>Cambia a ogni scrittura sulle aste: chi tiene una copia in memoria la confronta con questo.</summary>
public long AuctionsVersion => Interlocked.Read(ref _auctionsVersion);
private AuctionDatabase(string path) : base(path, SchemaV1, SchemaVersion, "AutoBidder.Database") { }
public static AuctionDatabase Open(string path) => new(path);
protected override void OnOpened(SqliteConnection writer)
{
// Gli id delle decisioni si assegnano qui, non da SQLite: chi registra una
// decisione deve poterla legare alla puntata senza aspettare il disco.
try { _nextDecisionId = writer.Query("SELECT COALESCE(MAX(decision_id), 0) AS n FROM bot_decisions")[0].Long("n"); }
catch { _nextDecisionId = 0; }
}
protected override void OnStatementExecuted(string sql)
{
if (sql.Contains("auctions", StringComparison.Ordinal)) Interlocked.Increment(ref _auctionsVersion);
}
/// <summary>Il prossimo id per <c>bot_decisions</c>: unico, crescente, senza aspettare la scrittura.</summary>
public long NextDecisionId() => Interlocked.Increment(ref _nextDecisionId);
/// <summary>
/// Svuota le tabelle delle osservazioni e delle decisioni. Restano i prodotti
/// (portano il valore reale scelto dall'utente) e le sessioni.
/// </summary>
public void WipeObservations()
{
EnqueueBatch(new (string, object?[])[]
{
("DELETE FROM auctions", Array.Empty<object?>()),
("DELETE FROM bot_decisions", Array.Empty<object?>()),
("DELETE FROM network_metrics", Array.Empty<object?>()),
("DELETE FROM bidders", Array.Empty<object?>()),
("DELETE FROM risk_ledger", Array.Empty<object?>())
});
Flush();
Vacuum();
}
private const string SchemaV1 = """
CREATE TABLE IF NOT EXISTS meta(
key TEXT PRIMARY KEY,
value TEXT);
-- Un prodotto: ciò che le aste hanno in comune. market_price_est è il valore
-- reale V scelto dall'utente; retail_price_bidoo è il «Compralo Ora».
CREATE TABLE IF NOT EXISTS items(
item_id INTEGER PRIMARY KEY,
product_key TEXT NOT NULL UNIQUE,
title TEXT NOT NULL DEFAULT '',
category TEXT,
retail_price_bidoo REAL,
market_price_est REAL,
market_price_source TEXT,
shipping REAL,
first_seen_at TEXT NOT NULL,
updated_at TEXT NOT NULL);
-- Un'asta: una riga sola, aggiornata dall'apertura alla chiusura. Le colonne
-- del riepilogo restano NULL finché l'asta è in corso.
CREATE TABLE IF NOT EXISTS auctions(
auction_id TEXT PRIMARY KEY,
item_id INTEGER REFERENCES items(item_id),
product_key TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT 'normal',
status TEXT NOT NULL DEFAULT 'open',
me TEXT,
app_version TEXT,
added_at TEXT NOT NULL,
first_seen_at TEXT NOT NULL,
opened_at TEXT NOT NULL,
closed_at TEXT,
end_time TEXT,
final_price REAL,
winner_user TEXT,
won_by_me INTEGER NOT NULL DEFAULT 0,
outcome TEXT,
final_status TEXT,
bid_increment REAL NOT NULL DEFAULT 0.01,
bid_fee REAL,
shipping REAL,
transaction_fee REAL,
buy_now_price REAL,
market_value REAL,
has_win_limit INTEGER NOT NULL DEFAULT 0,
win_limit TEXT,
my_bids INTEGER NOT NULL DEFAULT 0,
winner_bids_paid INTEGER,
winner_bids_free INTEGER,
resets INTEGER NOT NULL DEFAULT 0,
distinct_bidders INTEGER NOT NULL DEFAULT 0,
total_observed_bids INTEGER NOT NULL DEFAULT 0,
top_bidder_share REAL,
observed_from_start INTEGER NOT NULL DEFAULT 0,
observed_to_end INTEGER NOT NULL DEFAULT 0,
observed_minutes REAL,
avg_ping_ms REAL,
p95_ping_ms REAL,
polls INTEGER NOT NULL DEFAULT 0,
poll_errors INTEGER NOT NULL DEFAULT 0,
configured_lead_ms INTEGER,
lead_is_manual INTEGER NOT NULL DEFAULT 0,
timer_expired INTEGER NOT NULL DEFAULT 0,
successful_bids INTEGER NOT NULL DEFAULT 0,
failed_bids INTEGER NOT NULL DEFAULT 0,
price_velocity_per_minute REAL,
price_series_json TEXT,
bids_by_user_json TEXT,
config_json TEXT,
abandon_reason TEXT,
learned_at TEXT);
CREATE INDEX IF NOT EXISTS ix_auctions_end ON auctions(end_time);
CREATE INDEX IF NOT EXISTS ix_auctions_product ON auctions(product_key);
CREATE INDEX IF NOT EXISTS ix_auctions_status ON auctions(status);
-- Ogni puntata di ogni utente, come la riporta Bidoo. Il prezzo dopo la
-- puntata la identifica: ogni puntata alza il prezzo di un centesimo esatto.
CREATE TABLE IF NOT EXISTS bids(
bid_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
username TEXT NOT NULL,
price_after REAL NOT NULL,
bid_type TEXT NOT NULL DEFAULT '—',
is_mine INTEGER NOT NULL DEFAULT 0,
server_ts INTEGER,
seen_t REAL,
local_ts TEXT,
timer_before REAL,
rtt_ms INTEGER,
clock_offset_ms INTEGER,
UNIQUE(auction_id, price_after));
CREATE INDEX IF NOT EXISTS ix_bids_user ON bids(username);
CREATE INDEX IF NOT EXISTS ix_bids_server_ts ON bids(auction_id, server_ts);
-- Le interrogazioni, già coalescenti: una riga quando cambia qualcosa, e
-- comunque una al secondo perché il ping resti misurabile.
CREATE TABLE IF NOT EXISTS polls(
poll_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
price REAL,
timer REAL,
status TEXT,
last_bidder TEXT,
is_mine INTEGER NOT NULL DEFAULT 0,
ping_ms INTEGER,
expiry_unix INTEGER,
server_unix INTEGER,
clock_offset_ms INTEGER);
CREATE INDEX IF NOT EXISTS ix_polls_auction ON polls(auction_id, t);
-- Un reset del timer: chiude un ciclo, di cui si sa fin dove è sceso il
-- cronometro e quanto è durato.
CREATE TABLE IF NOT EXISTS resets(
reset_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
reset_count INTEGER,
price REAL,
bidder TEXT,
min_timer REAL,
cycle_seconds REAL);
CREATE INDEX IF NOT EXISTS ix_resets_auction ON resets(auction_id, t);
-- Le nostre puntate, con anticipo voluto e ottenuto: la coppia su cui si
-- tara il modello di latenza.
CREATE TABLE IF NOT EXISTS my_bids(
id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
price REAL,
planned_lead_ms INTEGER,
actual_lead_ms REAL,
ping_ms INTEGER,
rtt_ms INTEGER,
success INTEGER NOT NULL DEFAULT 0,
error TEXT,
bids_used INTEGER,
remaining_bids INTEGER,
decision_id INTEGER);
CREATE INDEX IF NOT EXISTS ix_my_bids_auction ON my_bids(auction_id, t);
-- Il registro dell'asta (strategie, avvisi, errori), riga per riga.
CREATE TABLE IF NOT EXISTS auction_log(
id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
level TEXT,
category TEXT,
msg TEXT);
CREATE INDEX IF NOT EXISTS ix_log_auction ON auction_log(auction_id, t);
-- Ogni decisione del motore, in Osserva (shadow) come in Attiva (live):
-- cosa avrebbe fatto o ha fatto, con che stima e per quale motivo.
CREATE TABLE IF NOT EXISTS bot_decisions(
decision_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL,
ts TEXT NOT NULL,
t REAL,
price REAL,
timer REAL,
action TEXT NOT NULL,
p_win REAL,
ev_estimate REAL,
model_version TEXT,
policy TEXT,
regime TEXT,
mode TEXT NOT NULL,
executed INTEGER NOT NULL DEFAULT 0,
outcome TEXT,
reason_detail TEXT,
state_json TEXT,
context_key TEXT,
alt_policy TEXT,
alt_action TEXT,
alt_p REAL);
CREATE INDEX IF NOT EXISTS ix_decisions_auction ON bot_decisions(auction_id);
CREATE INDEX IF NOT EXISTS ix_decisions_ts ON bot_decisions(ts);
-- Misure di rete aggregate a finestre di dieci secondi.
CREATE TABLE IF NOT EXISTS network_metrics(
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL,
rtt_ms INTEGER,
jitter_ms INTEGER,
p95_ms INTEGER,
clock_offset_ms INTEGER,
samples INTEGER,
source TEXT);
CREATE INDEX IF NOT EXISTS ix_network_ts ON network_metrics(ts);
-- Chi punta: aggregato asta per asta alla chiusura.
CREATE TABLE IF NOT EXISTS bidders(
username TEXT PRIMARY KEY,
first_seen TEXT,
last_seen TEXT,
n_auctions INTEGER NOT NULL DEFAULT 0,
n_bids INTEGER NOT NULL DEFAULT 0,
n_auto INTEGER NOT NULL DEFAULT 0,
n_manual INTEGER NOT NULL DEFAULT 0,
win_count INTEGER NOT NULL DEFAULT 0,
aggressiveness_score REAL,
profile_class TEXT);
CREATE TABLE IF NOT EXISTS sessions(
session_id INTEGER PRIMARY KEY,
started_at TEXT NOT NULL,
ended_at TEXT,
username TEXT,
app_version TEXT,
notes TEXT);
-- Uscite ed entrate in euro, per il gestore del rischio: ogni puntata
-- riuscita è una riga, ogni vincita è una riga.
CREATE TABLE IF NOT EXISTS risk_ledger(
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL,
kind TEXT NOT NULL,
amount_euro REAL NOT NULL,
auction_id TEXT,
note TEXT);
CREATE INDEX IF NOT EXISTS ix_ledger_ts ON risk_ledger(ts);
""";
}
}