Le aste non si salvano più in un file JSON per asta più tre archivi da tenere allineati: un solo database SQLite (winsqlite3.dll di Windows via P/Invoke, nessun pacchetto) con prodotti, aste, puntate, interrogazioni coalescenti, reset con profondità del ciclo, nostre puntate, decisioni del motore, misure di rete, puntatori, sessioni e contabilità. Scrittura in coda su un thread dedicato, letture in WAL. Per scelta dell'utente si parte da zero: il database si riempie man mano che si osservano le aste; i vecchi dossier restano leggibili per un'importazione futura ma non si scrivono più. Storico, schede, apprendimento, rigiocata ed esportazioni leggono tutti da lì. Il documento di progetto (Modifiche.txt) tradotto in C# senza librerie: - RiskManager: kill-switch (file KILL_SWITCH, anche dalla barra), HALT persistente per stop-loss e drawdown, tetto del giorno, aste in gioco insieme, contabilità in euro. - Theory: valore atteso con fee, spedizione, valore reale per prodotto (nuova colonna nella scheda Prodotti) e copertura «Compralo Ora»; null-model; sopravvivenza empirica e Kaplan-Meier dai reset; arrivi di Poisson. - Shadow per asta: in Osserva il cecchino arriva allo stesso istante, registra cosa avrebbe fatto e non punta; alla chiusura ogni decisione riceve l'esito, e ShadowReport confronta le policy sugli stessi istanti con intervallo di confidenza sul ROI. - NeuralNet (MLP, Adam) come sfidante del logistico, scelto dal Brier prequenziale; ThompsonBandit come seconda policy che impara dagli esiti delle decisioni; PennyAuctionEnv con avversari tarati sul database e QLearningAgent conservativo; SimulationLab per il confronto fra policy; esportazione CSV delle decisioni con reason_detail. Tutto nella scheda Apprendimento. Ml/LEGGIMI.md riscritto con le avvertenze su termini d'uso, quadro legale e realtà economica, lo schema del database e lo stato delle milestone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
496 lines
19 KiB
C#
496 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace AutoBidder.Data
|
|
{
|
|
/// <summary>
|
|
/// SQLite senza librerie: si chiama direttamente <c>winsqlite3.dll</c>, la copia di
|
|
/// SQLite che Windows 10 e 11 tengono in <c>System32</c> per i propri componenti.
|
|
///
|
|
/// <para><b>Perché così.</b> L'applicazione non deve dipendere da pacchetti esterni.
|
|
/// Un provider ADO.NET porterebbe con sé quattro assembly e una DLL nativa; qui ci
|
|
/// sono le quindici funzioni C che servono davvero, avvolte in due classi che non
|
|
/// fanno niente di più di quello che il programma usa: aprire, preparare, legare
|
|
/// parametri, scorrere righe, chiudere.</para>
|
|
///
|
|
/// <para><b>Fili.</b> Ogni connessione è usata da un thread alla volta: il database
|
|
/// scrive da un thread suo e legge sotto un lucchetto (vedi <see cref="AuctionDatabase"/>).
|
|
/// La connessione è comunque aperta in modalità serializzata, per non dipendere dalla
|
|
/// disciplina del chiamante.</para>
|
|
/// </summary>
|
|
internal static class SqliteNative
|
|
{
|
|
private const string Lib = "winsqlite3.dll";
|
|
|
|
public const int Ok = 0;
|
|
public const int Row = 100;
|
|
public const int Done = 101;
|
|
public const int Busy = 5;
|
|
public const int Locked = 6;
|
|
|
|
public const int OpenReadWrite = 0x2;
|
|
public const int OpenCreate = 0x4;
|
|
public const int OpenFullMutex = 0x10000;
|
|
|
|
public const int TypeInteger = 1;
|
|
public const int TypeFloat = 2;
|
|
public const int TypeText = 3;
|
|
public const int TypeBlob = 4;
|
|
public const int TypeNull = 5;
|
|
|
|
/// <summary>SQLITE_TRANSIENT: SQLite copia il testo legato prima che il buffer venga liberato.</summary>
|
|
public static readonly IntPtr Transient = new(-1);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_open_v2(byte[] filename, out IntPtr db, int flags, IntPtr vfs);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_close_v2(IntPtr db);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_prepare_v2(IntPtr db, byte[] sql, int nByte, out IntPtr stmt, out IntPtr tail);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_step(IntPtr stmt);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_reset(IntPtr stmt);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_clear_bindings(IntPtr stmt);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_finalize(IntPtr stmt);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_bind_int64(IntPtr stmt, int index, long value);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_bind_double(IntPtr stmt, int index, double value);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_bind_null(IntPtr stmt, int index);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_bind_text(IntPtr stmt, int index, byte[] text, int nBytes, IntPtr destructor);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_column_count(IntPtr stmt);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_column_type(IntPtr stmt, int col);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern long sqlite3_column_int64(IntPtr stmt, int col);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern double sqlite3_column_double(IntPtr stmt, int col);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern IntPtr sqlite3_column_text(IntPtr stmt, int col);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_column_bytes(IntPtr stmt, int col);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern IntPtr sqlite3_column_name(IntPtr stmt, int col);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern IntPtr sqlite3_errmsg(IntPtr db);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern long sqlite3_last_insert_rowid(IntPtr db);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_changes(IntPtr db);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern int sqlite3_busy_timeout(IntPtr db, int ms);
|
|
|
|
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
|
|
public static extern IntPtr sqlite3_libversion();
|
|
|
|
public static byte[] Utf8Z(string s)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(s);
|
|
var z = new byte[bytes.Length + 1];
|
|
Buffer.BlockCopy(bytes, 0, z, 0, bytes.Length);
|
|
return z;
|
|
}
|
|
|
|
public static string? Utf8(IntPtr p) => p == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(p);
|
|
}
|
|
|
|
public sealed class SqliteException : Exception
|
|
{
|
|
public int Code { get; }
|
|
|
|
public SqliteException(int code, string message) : base(message) => Code = code;
|
|
}
|
|
|
|
/// <summary>Una connessione. Da usare da un thread alla volta.</summary>
|
|
public sealed class SqliteConnection : IDisposable
|
|
{
|
|
private IntPtr _db;
|
|
private readonly Dictionary<string, SqliteStatement> _cache = new(StringComparer.Ordinal);
|
|
|
|
public string Path { get; }
|
|
|
|
public static string LibraryVersion
|
|
{
|
|
get
|
|
{
|
|
try { return SqliteNative.Utf8(SqliteNative.sqlite3_libversion()) ?? "?"; }
|
|
catch (DllNotFoundException) { return "winsqlite3.dll non trovata"; }
|
|
}
|
|
}
|
|
|
|
public SqliteConnection(string path)
|
|
{
|
|
Path = path;
|
|
|
|
var rc = SqliteNative.sqlite3_open_v2(
|
|
SqliteNative.Utf8Z(path), out _db,
|
|
SqliteNative.OpenReadWrite | SqliteNative.OpenCreate | SqliteNative.OpenFullMutex,
|
|
IntPtr.Zero);
|
|
|
|
if (rc != SqliteNative.Ok)
|
|
{
|
|
var msg = _db != IntPtr.Zero ? ErrorMessage() : $"codice {rc}";
|
|
if (_db != IntPtr.Zero) SqliteNative.sqlite3_close_v2(_db);
|
|
_db = IntPtr.Zero;
|
|
throw new SqliteException(rc, $"apertura di {path} non riuscita: {msg}");
|
|
}
|
|
|
|
// Un altro processo (o l'altra connessione) può tenere il file per qualche
|
|
// millisecondo: si aspetta invece di fallire.
|
|
SqliteNative.sqlite3_busy_timeout(_db, 5000);
|
|
}
|
|
|
|
internal IntPtr Handle => _db;
|
|
|
|
public string ErrorMessage() => SqliteNative.Utf8(SqliteNative.sqlite3_errmsg(_db)) ?? "errore sconosciuto";
|
|
|
|
public long LastInsertRowId => SqliteNative.sqlite3_last_insert_rowid(_db);
|
|
|
|
public int Changes => SqliteNative.sqlite3_changes(_db);
|
|
|
|
/// <summary>Esegue un'istruzione senza risultati (o ne scarta le righe).</summary>
|
|
public void Execute(string sql, params object?[] args)
|
|
{
|
|
using var stmt = Prepare(sql);
|
|
stmt.Bind(args);
|
|
stmt.RunToEnd();
|
|
}
|
|
|
|
/// <summary>Esegue più istruzioni separate da punto e virgola, senza parametri.</summary>
|
|
public void ExecuteScript(string script)
|
|
{
|
|
var bytes = SqliteNative.Utf8Z(script);
|
|
var offset = 0;
|
|
|
|
while (offset < bytes.Length - 1)
|
|
{
|
|
var remaining = new byte[bytes.Length - offset];
|
|
Buffer.BlockCopy(bytes, offset, remaining, 0, remaining.Length);
|
|
|
|
var rc = SqliteNative.sqlite3_prepare_v2(_db, remaining, -1, out var stmt, out var tail);
|
|
if (rc != SqliteNative.Ok) throw new SqliteException(rc, ErrorMessage());
|
|
|
|
// Il puntatore alla coda restituito da SQLite riguarda una copia nativa
|
|
// del buffer di cui non abbiamo la base: si ricava quanto è stato
|
|
// consumato scandendo fino al primo ';' fuori da apici e commenti, che è
|
|
// esattamente dove SQLite si è fermato.
|
|
var consumed = IndexAfterStatement(remaining);
|
|
|
|
if (stmt != IntPtr.Zero)
|
|
{
|
|
using var s = new SqliteStatement(this, stmt, "");
|
|
s.RunToEnd();
|
|
}
|
|
|
|
offset += Math.Max(1, consumed);
|
|
}
|
|
}
|
|
|
|
/// <summary>Fino al primo ';' fuori da apici e commenti, incluso.</summary>
|
|
private static int IndexAfterStatement(byte[] bytes)
|
|
{
|
|
var inQuote = false;
|
|
var inLineComment = false;
|
|
for (var i = 0; i < bytes.Length - 1; i++)
|
|
{
|
|
var c = (char)bytes[i];
|
|
if (inLineComment) { if (c == '\n') inLineComment = false; continue; }
|
|
if (c == '\'') inQuote = !inQuote;
|
|
else if (!inQuote && c == '-' && i + 1 < bytes.Length && bytes[i + 1] == '-') inLineComment = true;
|
|
else if (!inQuote && c == ';') return i + 1;
|
|
}
|
|
return bytes.Length - 1;
|
|
}
|
|
|
|
/// <summary>Prepara un'istruzione nuova, non condivisa. Chi la chiede la libera.</summary>
|
|
public SqliteStatement Prepare(string sql)
|
|
{
|
|
var rc = SqliteNative.sqlite3_prepare_v2(_db, SqliteNative.Utf8Z(sql), -1, out var stmt, out _);
|
|
if (rc != SqliteNative.Ok || stmt == IntPtr.Zero)
|
|
throw new SqliteException(rc, $"{ErrorMessage()} — SQL: {sql}");
|
|
|
|
return new SqliteStatement(this, stmt, sql);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Un'istruzione preparata e riusata: le stesse dieci frasi vengono eseguite
|
|
/// migliaia di volte, e prepararle ogni volta costerebbe più dell'esecuzione.
|
|
/// Viene azzerata prima di ogni uso.
|
|
/// </summary>
|
|
public SqliteStatement Cached(string sql)
|
|
{
|
|
if (!_cache.TryGetValue(sql, out var stmt))
|
|
{
|
|
stmt = Prepare(sql);
|
|
stmt.Shared = true;
|
|
_cache[sql] = stmt;
|
|
}
|
|
|
|
stmt.Reset();
|
|
return stmt;
|
|
}
|
|
|
|
public List<SqliteRow> Query(string sql, params object?[] args)
|
|
{
|
|
var stmt = Cached(sql);
|
|
stmt.Bind(args);
|
|
return stmt.ReadAll();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var s in _cache.Values) s.Shared = false;
|
|
foreach (var s in _cache.Values) s.Dispose();
|
|
_cache.Clear();
|
|
|
|
if (_db != IntPtr.Zero)
|
|
{
|
|
SqliteNative.sqlite3_close_v2(_db);
|
|
_db = IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Un'istruzione preparata.</summary>
|
|
public sealed class SqliteStatement : IDisposable
|
|
{
|
|
private readonly SqliteConnection _conn;
|
|
private IntPtr _stmt;
|
|
private string[]? _columns;
|
|
|
|
public string Sql { get; }
|
|
|
|
/// <summary>Appartiene alla cache della connessione: Dispose non la libera.</summary>
|
|
internal bool Shared { get; set; }
|
|
|
|
internal SqliteStatement(SqliteConnection conn, IntPtr stmt, string sql)
|
|
{
|
|
_conn = conn;
|
|
_stmt = stmt;
|
|
Sql = sql;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
SqliteNative.sqlite3_reset(_stmt);
|
|
SqliteNative.sqlite3_clear_bindings(_stmt);
|
|
}
|
|
|
|
/// <summary>Lega i parametri posizionali (?1, ?2, …) nell'ordine dato.</summary>
|
|
public void Bind(object?[] args)
|
|
{
|
|
for (var i = 0; i < args.Length; i++)
|
|
{
|
|
var idx = i + 1;
|
|
var rc = args[i] switch
|
|
{
|
|
null => SqliteNative.sqlite3_bind_null(_stmt, idx),
|
|
string s => BindText(idx, s),
|
|
bool b => SqliteNative.sqlite3_bind_int64(_stmt, idx, b ? 1 : 0),
|
|
int n => SqliteNative.sqlite3_bind_int64(_stmt, idx, n),
|
|
long l => SqliteNative.sqlite3_bind_int64(_stmt, idx, l),
|
|
double d => double.IsNaN(d) || double.IsInfinity(d)
|
|
? SqliteNative.sqlite3_bind_null(_stmt, idx)
|
|
: SqliteNative.sqlite3_bind_double(_stmt, idx, d),
|
|
float f => SqliteNative.sqlite3_bind_double(_stmt, idx, f),
|
|
decimal m => SqliteNative.sqlite3_bind_double(_stmt, idx, (double)m),
|
|
DateTime dt => BindText(idx, AuctionDatabase.Iso(dt)),
|
|
DateTimeOffset dto => BindText(idx, dto.UtcDateTime.ToString("o")),
|
|
Enum e => BindText(idx, e.ToString()),
|
|
_ => BindText(idx, args[i]!.ToString() ?? "")
|
|
};
|
|
|
|
if (rc != SqliteNative.Ok)
|
|
throw new SqliteException(rc, $"{_conn.ErrorMessage()} — parametro {idx} di: {Sql}");
|
|
}
|
|
}
|
|
|
|
private int BindText(int idx, string s)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(s);
|
|
return SqliteNative.sqlite3_bind_text(_stmt, idx, bytes, bytes.Length, SqliteNative.Transient);
|
|
}
|
|
|
|
/// <summary>Avanza di una riga. False alla fine.</summary>
|
|
public bool Step()
|
|
{
|
|
var rc = SqliteNative.sqlite3_step(_stmt);
|
|
if (rc == SqliteNative.Row) return true;
|
|
if (rc == SqliteNative.Done) return false;
|
|
throw new SqliteException(rc, $"{_conn.ErrorMessage()} — SQL: {Sql}");
|
|
}
|
|
|
|
public void RunToEnd()
|
|
{
|
|
while (Step()) { }
|
|
}
|
|
|
|
public string[] Columns
|
|
{
|
|
get
|
|
{
|
|
if (_columns != null) return _columns;
|
|
var n = SqliteNative.sqlite3_column_count(_stmt);
|
|
_columns = new string[n];
|
|
for (var i = 0; i < n; i++)
|
|
_columns[i] = SqliteNative.Utf8(SqliteNative.sqlite3_column_name(_stmt, i)) ?? $"c{i}";
|
|
return _columns;
|
|
}
|
|
}
|
|
|
|
public object? Value(int col)
|
|
{
|
|
switch (SqliteNative.sqlite3_column_type(_stmt, col))
|
|
{
|
|
case SqliteNative.TypeInteger: return SqliteNative.sqlite3_column_int64(_stmt, col);
|
|
case SqliteNative.TypeFloat: return SqliteNative.sqlite3_column_double(_stmt, col);
|
|
case SqliteNative.TypeText:
|
|
var p = SqliteNative.sqlite3_column_text(_stmt, col);
|
|
var n = SqliteNative.sqlite3_column_bytes(_stmt, col);
|
|
return p == IntPtr.Zero ? "" : Marshal.PtrToStringUTF8(p, n);
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
public List<SqliteRow> ReadAll()
|
|
{
|
|
var rows = new List<SqliteRow>();
|
|
var columns = Columns;
|
|
var index = SqliteRow.IndexFor(columns);
|
|
|
|
while (Step())
|
|
{
|
|
var values = new object?[columns.Length];
|
|
for (var i = 0; i < values.Length; i++) values[i] = Value(i);
|
|
rows.Add(new SqliteRow(index, values));
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Shared) return;
|
|
if (_stmt != IntPtr.Zero)
|
|
{
|
|
SqliteNative.sqlite3_finalize(_stmt);
|
|
_stmt = IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Una riga letta, con accesso per nome di colonna e conversioni tolleranti.</summary>
|
|
public sealed class SqliteRow
|
|
{
|
|
private readonly Dictionary<string, int> _index;
|
|
private readonly object?[] _values;
|
|
|
|
private static readonly Dictionary<string, Dictionary<string, int>> IndexCache = new(StringComparer.Ordinal);
|
|
|
|
internal static Dictionary<string, int> IndexFor(string[] columns)
|
|
{
|
|
var key = string.Join("|", columns);
|
|
lock (IndexCache)
|
|
{
|
|
if (!IndexCache.TryGetValue(key, out var idx))
|
|
{
|
|
idx = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
for (var i = 0; i < columns.Length; i++) idx[columns[i]] = i;
|
|
IndexCache[key] = idx;
|
|
}
|
|
return idx;
|
|
}
|
|
}
|
|
|
|
internal SqliteRow(Dictionary<string, int> index, object?[] values)
|
|
{
|
|
_index = index;
|
|
_values = values;
|
|
}
|
|
|
|
public object? this[string column] => _index.TryGetValue(column, out var i) ? _values[i] : null;
|
|
|
|
/// <summary>Valore per posizione: per i risultati scalari.</summary>
|
|
public object? At(int i) => i >= 0 && i < _values.Length ? _values[i] : null;
|
|
|
|
public bool Has(string column) => _index.ContainsKey(column);
|
|
|
|
public string Str(string column) => this[column]?.ToString() ?? "";
|
|
|
|
public string? StrOrNull(string column) => this[column] as string;
|
|
|
|
public long Long(string column) => this[column] switch
|
|
{
|
|
long l => l,
|
|
double d => (long)d,
|
|
string s when long.TryParse(s, out var v) => v,
|
|
_ => 0
|
|
};
|
|
|
|
public int Int(string column) => (int)Long(column);
|
|
|
|
public bool Bool(string column) => Long(column) != 0;
|
|
|
|
public double Dbl(string column) => this[column] switch
|
|
{
|
|
double d => d,
|
|
long l => l,
|
|
string s when double.TryParse(s, System.Globalization.NumberStyles.Float,
|
|
System.Globalization.CultureInfo.InvariantCulture, out var v) => v,
|
|
_ => 0
|
|
};
|
|
|
|
public double? DblOrNull(string column) => this[column] == null ? null : Dbl(column);
|
|
|
|
public int? IntOrNull(string column) => this[column] == null ? null : Int(column);
|
|
|
|
public long? LongOrNull(string column) => this[column] == null ? null : Long(column);
|
|
|
|
/// <summary>Data in UTC, o null. Le date sono scritte in ISO 8601 UTC.</summary>
|
|
public DateTime? Date(string column)
|
|
{
|
|
var s = this[column] as string;
|
|
if (string.IsNullOrEmpty(s)) return null;
|
|
if (!DateTime.TryParse(s, System.Globalization.CultureInfo.InvariantCulture,
|
|
System.Globalization.DateTimeStyles.RoundtripKind, out var d))
|
|
return null;
|
|
|
|
return d.Kind switch
|
|
{
|
|
DateTimeKind.Utc => d,
|
|
DateTimeKind.Local => d.ToUniversalTime(),
|
|
_ => DateTime.SpecifyKind(d, DateTimeKind.Utc) // scritto senza fuso: era UTC
|
|
};
|
|
}
|
|
}
|
|
}
|