Files
Mimante/Mimante/Core/EventHandlers/MainWindow.EventHandlers.Stats.cs
T
Alby96andClaude Fable 5.1 7993ca44a0 Database SQLite al posto dei dossier, gestore del rischio, decisioni in shadow, rete neurale, bandit e simulatore
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>
2026-09-08 11:00:49 +02:00

272 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Registrazione delle aste concluse.
///
/// Ne escono due archivi con scopi diversi: un riepilogo compatto che alimenta la
/// pagina Statistiche, e una scheda dettagliata per ogni asta destinata alle analisi
/// successive. Il secondo esiste perché quei dati, se non raccolti mentre l'asta è in
/// corso, non sono più recuperabili: Bidoo non li espone a posteriori.
/// </summary>
public partial class MainWindow
{
private void AuctionMonitor_OnAuctionCompleted(AuctionInfo auction, AuctionState state, bool won)
{
try
{
var settings = SettingsManager.Load();
var record = new CompletedAuctionRecord
{
AuctionId = auction.AuctionId,
Name = string.IsNullOrWhiteSpace(auction.Name) ? auction.AuctionId : auction.Name,
ProductKey = ProductKeyHelper.GenerateProductKey(auction.Name),
OriginalUrl = auction.OriginalUrl,
FinalPrice = state?.Price ?? 0,
Winner = state?.LastBidder ?? "",
WonByMe = won,
Outcome = won ? "Vinta"
: (state?.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa"),
MyBidsUsed = auction.BidsUsedOnThisAuction ?? 0,
TotalResets = auction.ResetCount,
BuyNowPrice = auction.BuyNowPrice,
AddedAt = auction.AddedAt,
// Le puntate spese dal vincitore arrivano dal server insieme allo
// stato finale: e' il numero che il sito mostra come "Puntate
// utilizzate", e l'unico affidabile (lo storico si ferma a cinquanta).
WinnerBidsPaid = state?.WinnerBidsPaid,
WinnerBidsFree = state?.WinnerBidsFree,
// L'ora della fine vera, non quella della registrazione. Un'asta
// terminata resta nel monitor finché non la si toglie: prendere
// DateTime.Now qui significherebbe datare la chiusura al momento in cui
// si è fatto pulizia, magari il giorno dopo.
EndedAt = ResolveEndedAt(auction, state)
};
CompletedAuctionsStore.Append(record);
// Un dato incoerente non va nelle medie: si dice e si scarta, invece di
// spostare in silenzio i limiti di prezzo del prodotto.
var integrity = AuctionIntegrity.CheckWinnerBids(record);
if (record.WinnerBidsUsed.HasValue && !integrity.IsTrustworthy)
{
Log($"[STATISTICHE] {record.Name}: puntate del vincitore scartate ({integrity.Reason})",
LogLevel.Warning);
}
else if (integrity.IsTrustworthy)
{
Log($"[STATISTICHE] {record.Name}: vinta con {record.WinnerBidsUsed} puntate " +
$"(prezzo {record.FinalPrice:F2} EUR)", LogLevel.Info);
}
// La scheda dettagliata vive solo nel riepilogo del dossier: l'archivio
// mensile che la duplicava non c'è più (vedi AuctionDetailStore).
var detail = BuildDetail(auction, state, record);
// La registrazione si chiude con il riepilogo: da lì in poi la riga è una
// storia completa, ed è così che l'analisi sa di poterla usare.
Data.AuctionRecorder.Close(auction, detail, state);
ProductStatsStore.RecordCompleted(record, detail);
// L'apprendimento impara subito da questa asta: il dossier è già su disco
// (Close lo svuota) e lo storico è già scritto. Fuori dal percorso di
// chiusura, che deve restare corto.
_ = System.Threading.Tasks.Task.Run(() => Ml.LearningService.OnAuctionClosed(record));
NotifyOutcome(record, settings);
Dispatcher.BeginInvoke(() =>
{
Log($"[STORICO] Asta '{record.Name}' salvata ({record.Outcome}, €{record.FinalPrice:F2})",
won ? LogLevel.Success : LogLevel.Info);
// Una vincita è il solo momento in cui il conto delle aste da confermare
// cambia: chiederlo subito evita che la barra resti indietro fino al
// giro successivo del timer.
if (won) _ = RefreshAuctionsToConfirmAsync();
if (StatisticsPanel != null && StatisticsPanel.Visibility == Visibility.Visible)
{
LoadStatistics();
}
// Ultimo passo, e non prima: lo storico è già scritto, il dossier
// chiuso, le statistiche del prodotto aggiornate. Togliere l'asta
// dall'elenco a questo punto non perde niente — tutto ciò che aveva
// da dire è già altrove.
RimuoviSeConclusaEDaTogliere(auction, won, settings);
});
}
catch (Exception ex)
{
Console.WriteLine($"[STORICO ERROR] {ex.Message}");
}
}
/// <summary>
/// Toglie dall'elenco l'asta appena conclusa, se la regola lo consente.
///
/// <para>Con l'aggiunta automatica accesa le aste concluse si accumulano a centinaia
/// e seppelliscono quelle vive, che è il contrario di quello che serve guardando un
/// cruscotto. Restano però quelle su cui si è speso: sono le uniche che vale la pena
/// riguardare, e i soldi erano veri. Vedi <see cref="FinishedAuctionCleanup"/>.</para>
///
/// <para>Il motivo finisce sempre nel registro, anche quando l'asta resta: senza,
/// un'asta che sparisce o che non sparisce sarebbero entrambe inspiegabili.</para>
/// </summary>
private void RimuoviSeConclusaEDaTogliere(AuctionInfo auction, bool won, AppSettings settings)
{
try
{
var verdict = FinishedAuctionCleanup.Decide(auction, won, new FinishedAuctionCleanup.Options
{
Enabled = settings.AutoRemoveFinished,
KeepWithMyBids = settings.AutoRemoveKeepWithMyBids,
KeepWon = settings.AutoRemoveKeepWon,
KeepUnclear = settings.AutoRemoveKeepUnclear
});
if (!verdict.Remove)
{
if (settings.AutoRemoveFinished)
Log($"[ELENCO] '{auction.Name}' resta in elenco: {verdict.Reason}", LogLevel.Info);
return;
}
// Stesso giro della rimozione a mano: prima il motore, poi la griglia,
// poi il salvataggio. Toglierla solo dalla griglia lascerebbe il runner
// a interrogare un'asta che non esiste piu' per chi guarda.
_auctionMonitor.RemoveAuction(auction.AuctionId);
var vm = _auctionViewModels.FirstOrDefault(x => x.AuctionId == auction.AuctionId);
if (vm != null) _auctionViewModels.Remove(vm);
SaveAuctions();
UpdateTotalCount();
UpdateGlobalControlButtons();
Log($"[ELENCO] '{auction.Name}' tolta dall'elenco: {verdict.Reason}", LogLevel.Info);
}
catch (Exception ex)
{
Console.WriteLine($"[ELENCO ERROR] {ex.Message}");
}
}
/// <summary>
/// Quando l'asta è finita davvero, in ordine di attendibilità: l'istante fissato dal
/// monitor quando ha visto la conclusione dal vivo; la scadenza dichiarata dal
/// server nell'ultima risposta; l'istante di quella risposta. Solo se non c'è
/// nessuna delle tre si ripiega sull'ora corrente.
///
/// <para>La differenza non è cosmetica: lo storico si legge per capire a che ora del
/// giorno chiudono le aste che conviene seguire, e una data presa al momento della
/// rimozione rende quel numero privo di significato.</para>
/// </summary>
private static DateTime ResolveEndedAt(AuctionInfo auction, AuctionState? state)
{
if (auction.ConcludedAt.HasValue) return auction.ConcludedAt.Value;
if (state != null && state.ExpiryUnixSeconds > 0)
return DateTimeOffset.FromUnixTimeSeconds(state.ExpiryUnixSeconds).ToLocalTime().DateTime;
if (state != null && state.SnapshotTime != default)
return state.SnapshotTime.ToLocalTime();
return DateTime.Now;
}
/// <summary>
/// Costruisce la scheda dettagliata a partire da ciò che il monitor ha osservato.
/// </summary>
private AuctionDetailRecord BuildDetail(AuctionInfo auction, AuctionState? state, CompletedAuctionRecord summary)
{
var bidders = auction.SnapshotBidderStats();
var bidsByUser = bidders.ToDictionary(b => b.Username, b => b.BidCount, StringComparer.OrdinalIgnoreCase);
var series = auction.SnapshotPriceSeries();
var firstSeen = auction.FirstSeenAt ?? auction.AddedAt;
var observedMinutes = Math.Max(0, (summary.EndedAt - firstSeen).TotalMinutes);
return new AuctionDetailRecord
{
AuctionId = summary.AuctionId,
Name = summary.Name,
ProductKey = summary.ProductKey,
Url = summary.OriginalUrl,
Outcome = summary.Outcome,
WonByMe = summary.WonByMe,
Winner = summary.Winner,
FinalPrice = summary.FinalPrice,
BuyNowPrice = auction.BuyNowPrice,
ShippingCost = auction.ShippingCost,
FirstSeenAt = firstSeen,
EndedAt = summary.EndedAt,
ObservedMinutes = observedMinutes,
MyBids = summary.MyBidsUsed,
TotalObservedBids = bidsByUser.Values.Sum(),
DistinctBidders = bidsByUser.Count,
Resets = auction.ResetCount,
BidsByUser = bidsByUser,
TopBidderShare = AuctionDetailRecord.ComputeTopShare(bidsByUser),
PriceSeries = series,
PriceVelocityPerMinute = observedMinutes > 0.01 ? summary.FinalPrice / observedMinutes : 0,
AveragePingMs = auction.AverageLatencyMs,
PollCount = auction.PollCount,
PollErrors = auction.PollErrors,
ConfiguredLeadMs = auction.BidBeforeDeadlineMs,
TimerExpiredCount = auction.TimerExpiredCount,
SuccessfulBids = auction.SuccessfulBidCount,
FailedBids = auction.FailedBidCount,
BidCostEuro = auction.BidCost,
ObservedFromStart = auction.ObservedFromStart,
ObservedToEnd = auction.ObservedToEnd
};
}
/// <summary>
/// Avvisa con una notifica di sistema. Una vittoria va confermata su Bidoo, e
/// accorgersene solo riaprendo la finestra è facile da dimenticare.
/// </summary>
private static void NotifyOutcome(CompletedAuctionRecord record, AppSettings settings)
{
try
{
if (record.WonByMe && settings.NotifyOnWin)
{
WindowsNotifier.Show(
"Asta vinta!",
$"{record.Name}\nAggiudicata a € {record.FinalPrice:F2} con {record.MyBidsUsed} puntate.\n" +
"Ricordati di completare l'acquisto su Bidoo.");
}
else if (!record.WonByMe && settings.NotifyOnLoss && record.MyBidsUsed > 0)
{
// Solo se ci avevamo davvero puntato: le aste solo osservate non
// meritano un avviso.
WindowsNotifier.Show(
"Asta persa",
$"{record.Name}\nChiusa a € {record.FinalPrice:F2} da {record.Winner}.",
warning: true);
}
}
catch { }
}
}
}