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>
166 lines
7.5 KiB
C#
166 lines
7.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using AutoBidder.Data;
|
|
using AutoBidder.Utilities;
|
|
|
|
namespace AutoBidder.Ml
|
|
{
|
|
/// <summary>
|
|
/// Cosa avrebbero reso le decisioni registrate, policy per policy, con l'esito noto.
|
|
///
|
|
/// <para>Ogni decisione ha un esito alla chiusura dell'asta: se il prezzo non è più
|
|
/// salito dopo quell'istante, una puntata lì sarebbe stata l'ultima. Con questo si
|
|
/// valuta ogni policy sugli stessi istanti, dal vivo e in shadow: quante volte
|
|
/// avrebbe puntato, quante di quelle sarebbero state vincenti, quanto avrebbe speso e
|
|
/// incassato al valore reale del bene. È la valutazione <i>off-policy</i> più onesta
|
|
/// che i dati permettono — con l'avvertenza che una puntata nostra può cambiare il
|
|
/// comportamento altrui, e questo nessun dato registrato lo dice.</para>
|
|
///
|
|
/// <para>Il confronto è sempre con il null-model: profitto atteso per puntata zero.
|
|
/// Un ROI per puntata positivo su poche decine di decisioni non prova nulla; su
|
|
/// migliaia, e con l'estremo inferiore dell'intervallo sopra lo zero, comincia a
|
|
/// dire qualcosa.</para>
|
|
/// </summary>
|
|
public static class ShadowReport
|
|
{
|
|
public sealed class PolicyStats
|
|
{
|
|
public string Name { get; init; } = "";
|
|
public int Decisions { get; set; }
|
|
public int Bids { get; set; }
|
|
public int Wins { get; set; }
|
|
public double Spent { get; set; }
|
|
public double Gained { get; set; }
|
|
public double ForecastEv { get; set; }
|
|
public readonly List<double> PerBid = new();
|
|
|
|
public double Net => Gained - Spent;
|
|
public double RoiPerBid => Bids > 0 ? Net / Bids : 0;
|
|
public double WinRate => Bids > 0 ? (double)Wins / Bids : 0;
|
|
|
|
/// <summary>Estremo inferiore (95%) del ROI per puntata: sotto zero, il null-model non è battuto.</summary>
|
|
public double RoiLower95
|
|
{
|
|
get
|
|
{
|
|
if (PerBid.Count < 2) return double.NaN;
|
|
var m = PerBid.Average();
|
|
var sd = Math.Sqrt(PerBid.Sum(x => (x - m) * (x - m)) / (PerBid.Count - 1));
|
|
return m - 1.96 * sd / Math.Sqrt(PerBid.Count);
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class Report
|
|
{
|
|
public int Days { get; init; }
|
|
public int TotalDecisions { get; init; }
|
|
public int WithOutcome { get; init; }
|
|
public int LiveBids { get; init; }
|
|
public int LiveWins { get; init; }
|
|
public List<PolicyStats> Policies { get; } = new();
|
|
public string Text { get; set; } = "";
|
|
}
|
|
|
|
public static Report Compute(AuctionDatabase db, AppSettings settings, int days = 30)
|
|
{
|
|
db.Flush();
|
|
var since = AuctionDatabase.Iso(DateTime.Today.AddDays(-days));
|
|
|
|
var rows = db.Query(
|
|
"SELECT d.mode, d.action, d.outcome, d.ev_estimate, d.price, d.alt_action, d.executed, " +
|
|
" COALESCE(a.market_value, a.buy_now_price) AS v, a.shipping, a.bid_fee " +
|
|
"FROM bot_decisions d JOIN auctions a ON a.auction_id = d.auction_id " +
|
|
"WHERE d.ts >= ?1 AND d.outcome IS NOT NULL", since);
|
|
|
|
var total = (int)db.ScalarLong("SELECT COUNT(*) AS n FROM bot_decisions WHERE ts >= ?1", since);
|
|
|
|
var acting = new PolicyStats { Name = "modello + regime (in uso)" };
|
|
var alt = new PolicyStats { Name = "bandit Thompson (shadow)" };
|
|
var always = new PolicyStats { Name = "punta sempre" };
|
|
var never = new PolicyStats { Name = "non punta mai" };
|
|
int liveBids = 0, liveWins = 0;
|
|
|
|
foreach (var r in rows)
|
|
{
|
|
var outcome = r.Str("outcome");
|
|
var unanswered = outcome is "won" or "would_win" or "missed";
|
|
var v = r.DblOrNull("v") ?? 0;
|
|
var price = r.Dbl("price");
|
|
var fee = settings.TransactionFeeEuro;
|
|
var ship = r.DblOrNull("shipping") ?? 0;
|
|
var cost = r.DblOrNull("bid_fee") is > 0 ? r.Dbl("bid_fee") : settings.AverageBidCostEuro;
|
|
|
|
var gainIfWin = v - (price + Theory.PriceIncrement) - fee - ship;
|
|
|
|
void Apply(PolicyStats s, bool bid, double? forecast)
|
|
{
|
|
s.Decisions++;
|
|
if (!bid) return;
|
|
s.Bids++;
|
|
s.Spent += cost;
|
|
if (forecast is { } f) s.ForecastEv += f;
|
|
if (unanswered)
|
|
{
|
|
s.Wins++;
|
|
s.Gained += gainIfWin;
|
|
s.PerBid.Add(gainIfWin - cost);
|
|
}
|
|
else s.PerBid.Add(-cost);
|
|
}
|
|
|
|
var actingBid = r.Str("action") == "BID";
|
|
Apply(acting, actingBid, r.DblOrNull("ev_estimate"));
|
|
Apply(alt, r.Str("alt_action") == "BID", null);
|
|
Apply(always, true, null);
|
|
Apply(never, false, null);
|
|
|
|
if (r.Str("mode") == "live" && r.Bool("executed"))
|
|
{
|
|
liveBids++;
|
|
if (outcome == "won") liveWins++;
|
|
}
|
|
}
|
|
|
|
var report = new Report
|
|
{
|
|
Days = days, TotalDecisions = total, WithOutcome = rows.Count, LiveBids = liveBids, LiveWins = liveWins
|
|
};
|
|
report.Policies.AddRange(new[] { acting, alt, always, never });
|
|
report.Text = Format(report);
|
|
return report;
|
|
}
|
|
|
|
public static string Format(Report r)
|
|
{
|
|
var sb = new StringBuilder();
|
|
var c = System.Globalization.CultureInfo.GetCultureInfo("it-IT");
|
|
|
|
sb.AppendLine($"Decisioni negli ultimi {r.Days} giorni: {r.TotalDecisions:N0}, con esito noto {r.WithOutcome:N0}. " +
|
|
$"Puntate dal vivo: {r.LiveBids:N0}, di cui vincenti {r.LiveWins:N0}.");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" policy decisioni puntate vincenti spesi € incassati € netto € ROI/puntata IC95% inf. EV previsto");
|
|
sb.AppendLine(" ------------------------------------------------------------------------------------------------------------------------");
|
|
|
|
foreach (var p in r.Policies)
|
|
{
|
|
sb.AppendLine(string.Format(c,
|
|
" {0,-30} {1,9} {2,8} {3,9} {4,9:F2} {5,12:F2} {6,9:F2} {7,12:F3} {8,10} {9,12}",
|
|
p.Name, p.Decisions, p.Bids, p.Wins, p.Spent, p.Gained, p.Net, p.RoiPerBid,
|
|
double.IsNaN(p.RoiLower95) ? "—" : p.RoiLower95.ToString("F3", c),
|
|
p.Name.Contains("in uso") ? p.ForecastEv.ToString("F2", c) : "—"));
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine(" Il null-model teorico (Augenblick 2016) prevede ROI per puntata = 0. Una policy batte il caso solo se");
|
|
sb.AppendLine(" l'estremo inferiore dell'intervallo di confidenza è sopra zero, e su abbastanza puntate da contare.");
|
|
sb.AppendLine(" «Vincenti» = il prezzo non è più salito dopo quell'istante: una puntata lì sarebbe stata l'ultima.");
|
|
sb.AppendLine(" Avvertenza: una puntata nostra può far rispondere chi stava per smettere; i dati non lo dicono.");
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
}
|