Files
Mimante/Mimante/Services/BidStrategyService.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

320 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using AutoBidder.Ml;
using AutoBidder.Models;
using AutoBidder.Utilities;
namespace AutoBidder.Services
{
/// <summary>
/// Decide se puntare. Cinque passi, sempre nello stesso ordine — vedi
/// <c>Ml/LEGGIMI.md</c> per il disegno completo e i numeri che lo giustificano.
///
/// <list type="number">
/// <item><b>Rischio</b> — il gestore del rischio: kill-switch, HALT, stop-loss,
/// drawdown, tetto del giorno, aste in gioco insieme. Solo dal vivo: in shadow la
/// decisione si registra com'è, per poterla giudicare.</item>
/// <item><b>Paletti</b> — regole dell'utente che non si discutono: tetti di
/// puntate, fascia oraria sospesa.</item>
/// <item><b>Duello</b> — riconoscimento dell'autopuntata avversaria dal ritmo
/// delle risposte.</item>
/// <item><b>Valore atteso</b> — il modello appreso dice con che probabilità una
/// puntata fatta adesso resterebbe senza risposta; <see cref="Theory"/> la
/// trasforma in euro con valore reale del bene, fee, spedizione e copertura.</item>
/// <item><b>Regime</b> — una macchina a stati per asta che trasforma la sequenza
/// dei valori attesi in una condotta: quando lasciar sfogare gli altri, quando
/// sondare, quando tornare a giocare.</item>
/// </list>
///
/// <para><i>Quando</i> puntare — l'anticipo — non si decide qui ma nel cecchino,
/// con <see cref="LatencyModel"/>.</para>
/// </summary>
public class BidStrategyService
{
private int _sessionTotalBids = 0;
private DateTime _sessionStartedAt = DateTime.UtcNow;
private readonly Random _rng = new();
/// <summary>
/// Il valore reale V del bene di quest'asta: quello scritto dall'utente sul
/// prodotto, se c'è; altrimenti il «Compralo Ora». Vedi <see cref="Theory"/>: il
/// listino di Bidoo è gonfiato, e un valore atteso calcolato su di esso mente.
/// </summary>
public static double? ValueOf(AuctionInfo auction)
{
if (auction == null) return null;
var scheda = WatchedProductsStore.FindByName(auction.Name);
return scheda?.MarketValue is > 0 ? scheda.MarketValue : auction.BuyNowPrice;
}
public BidDecision ShouldPlaceBid(
AuctionInfo auction,
AuctionState state,
AppSettings settings,
string currentUsername,
IReadOnlyList<AuctionInfo>? allAuctions = null,
bool shadow = false)
{
var decision = new BidDecision { ShouldBid = true, Policy = "ev-regime" };
// ── 1. Rischio ─────────────────────────────────────────────────
if (!shadow)
{
var risk = RiskManager.Authorize(auction, settings, allAuctions);
if (!risk.Allowed)
{
decision.ShouldBid = false;
decision.Reason = risk.Reason;
decision.Policy = "rischio";
return decision;
}
}
// ── 2. Paletti ─────────────────────────────────────────────────
if (QuietHoursApply(auction, settings) &&
BiddingHours.IsQuiet(DateTime.Now, settings.QuietHoursStart, settings.QuietHoursEnd))
{
decision.ShouldBid = false;
decision.Reason = BiddingHours.Spiegazione(settings.QuietHoursStart, settings.QuietHoursEnd);
decision.Policy = "paletti";
return decision;
}
if (settings.BankrollManagerEnabled)
{
var bankrollCheck = CheckBankrollLimits(auction, settings);
if (!bankrollCheck.CanBid)
{
decision.ShouldBid = false;
decision.Reason = bankrollCheck.Reason;
decision.Policy = "paletti";
return decision;
}
}
// ── 3. Duello con l'autopuntata ────────────────────────────────
if (auction.AutoBidDuelDetected)
{
auction.Regime.AutopuntataRiconosciuta();
if (settings.AutoBidDuelWithdrawEnabled)
{
decision.ShouldBid = false;
decision.Reason = AutoBidDuel.Spiegazione(auction.AutoResponsesInARow);
decision.Policy = "duello";
decision.Regime = auction.Regime.StatoAttuale.ToString();
return decision;
}
}
// ── 4. Valore atteso ───────────────────────────────────────────
//
// Il modello parla sempre; decide solo quando ha appreso abbastanza aste. La
// probabilità e il valore atteso finiscono comunque sull'asta e sulla decisione.
double? ev = null;
string? spiegazioneEv = null;
var pronto = false;
var value = ValueOf(auction);
if (settings.LearningGateEnabled && value is > 0)
{
var ctx = LearningService.BuildContext(auction, state, DateTime.Now, currentUsername);
var prob = ctx is { } c0 ? LearningService.PredictUnanswered(c0) : (double?)null;
if (prob is { } p && ctx is { } c)
{
var costo = settings.AverageBidCostEuro * Math.Max(0.1, settings.LearningEvMultiplier);
var speso = (auction.BidsUsedOnThisAuction ?? 0) * auction.BidCost;
var situazione = new Theory.BidSituation(
PWin: p,
Value: value.Value,
CurrentPrice: state.Price,
BidCost: costo,
TransactionFee: settings.TransactionFeeEuro,
Shipping: auction.ShippingCost ?? 0,
ListPrice: auction.BuyNowPrice,
BidsSpentEuro: speso,
HedgeEnabled: settings.BuyNowHedgeEnabled);
var valore = Theory.BidExpectedValue(situazione);
auction.LearnedUnansweredProbability = p;
auction.LearnedExpectedValue = valore;
pronto = LearningService.IsReady(settings);
ev = valore;
decision.Probability = p;
decision.ExpectedValue = valore;
var margine = value.Value - state.Price - Theory.PriceIncrement - settings.TransactionFeeEuro - (auction.ShippingCost ?? 0);
spiegazioneEv =
$"valore atteso negativo: P(senza risposta) {p:P2} × margine {margine:F2} € " +
$" puntata {costo:F2} €{(Theory.LossBranchCost(situazione) == 0 ? " (coperta)" : "")} = {valore:+0.000;-0.000} €";
LearningService.RecordDecision(auction.Name, state.Price, p, valore,
blocked: pronto && valore < 0, ready: pronto);
// La seconda policy, sempre in shadow: il bandit dice la sua sullo
// stesso istante, e alla chiusura entrambe vengono giudicate sugli esiti.
var key = ThompsonBandit.KeyOf(c);
var (altBid, altP) = LearningService.Bandit.Decide(key, situazione, _rng);
decision.ContextKey = key;
decision.AltAction = altBid ? "BID" : "NO-OP";
decision.AltP = altP;
}
}
// ── 5. Regime di concorrenza ───────────────────────────────────
if (pronto && ev is { } valoreAtteso)
{
var statoPrima = auction.Regime.StatoAttuale;
var consentito = auction.Regime.Osserva(valoreAtteso >= 0);
decision.Regime = auction.Regime.StatoAttuale.ToString();
if (auction.Regime.StatoAttuale != statoPrima)
{
auction.AddLog($"[REGIME] {statoPrima} → {auction.Regime.StatoAttuale}: {auction.Regime.Spiegazione()}",
AuctionLogLevel.Strategy, AuctionLogCategory.Strategy);
}
if (!consentito)
{
decision.ShouldBid = false;
decision.Reason = auction.Regime.StatoAttuale == CompetitionRegime.Stato.Calmo
? spiegazioneEv
: auction.Regime.Spiegazione();
return decision;
}
decision.Reason = $"valore atteso {valoreAtteso:+0.000;-0.000} € (P {ev:P2}), regime {auction.Regime.StatoAttuale}";
}
else if (ev is { } evNonPronto)
{
decision.Reason = $"modello non ancora pronto ({LearningService.AuctionsLearned}/{settings.LearningMinAuctions} aste): parere EV {evNonPronto:+0.000;-0.000} €, si punta";
}
else
{
decision.Reason = "nessuna stima disponibile (valore ignoto o nessuna puntata recente): si punta";
}
return decision;
}
private BankrollCheckResult CheckBankrollLimits(AuctionInfo auction, AppSettings settings)
{
var result = new BankrollCheckResult { CanBid = true };
var maxPerAuction = auction.MaxBidsOverride ?? settings.MaxBidsPerAuction;
if (maxPerAuction > 0 && auction.SessionBidCount >= maxPerAuction)
{
result.CanBid = false;
result.Reason = $"Limite puntate per asta raggiunto ({auction.SessionBidCount}/{maxPerAuction})";
return result;
}
if (settings.MaxBidsPerSession > 0 && _sessionTotalBids >= settings.MaxBidsPerSession)
{
result.CanBid = false;
result.Reason = $"Limite puntate per sessione raggiunto ({_sessionTotalBids}/{settings.MaxBidsPerSession})";
return result;
}
return result;
}
/// <summary>
/// La sospensione a fascia oraria vale per quest'asta? Tre livelli, dal più
/// specifico al più generale: l'asta scavalca il prodotto, il prodotto scavalca
/// le impostazioni generali.
/// </summary>
public static bool QuietHoursApply(AuctionInfo auction, AppSettings settings)
{
if (auction?.QuietHoursEnabledOverride is { } perAsta) return perAsta;
var scheda = WatchedProductsStore.FindByName(auction?.Name);
if (scheda?.QuietHoursEnabled is { } perProdotto) return perProdotto;
return settings.QuietHoursEnabled;
}
/// <summary>Registra una puntata inviata, riuscita o no.</summary>
public void RecordBidAttempt(AuctionInfo auction, bool success)
{
auction.SessionBidCount++;
_sessionTotalBids++;
if (success)
{
auction.SuccessfulBidCount++;
auction.LastMyBidAtUtc = DateTime.UtcNow;
auction.Regime.PuntataPiazzata();
}
else
{
auction.FailedBidCount++;
}
}
/// <summary>Un ciclo perso perche' la puntata e' arrivata a giochi chiusi.</summary>
public void RecordTimerExpired(AuctionInfo auction)
{
auction.TimerExpiredCount++;
}
public void ResetSession()
{
_sessionTotalBids = 0;
_sessionStartedAt = DateTime.UtcNow;
}
public SessionStats GetSessionStats()
{
return new SessionStats
{
TotalBids = _sessionTotalBids,
SessionDuration = DateTime.UtcNow - _sessionStartedAt
};
}
}
/// <summary>Il verdetto della strategia, con i numeri che l'hanno prodotto.</summary>
public class BidDecision
{
public bool ShouldBid { get; set; }
public string? Reason { get; set; }
/// <summary>P(senza risposta) stimata dal modello, se c'era.</summary>
public double? Probability { get; set; }
/// <summary>Valore atteso in euro della puntata, se c'era.</summary>
public double? ExpectedValue { get; set; }
/// <summary>Regime di concorrenza al momento della decisione.</summary>
public string? Regime { get; set; }
/// <summary>Quale parte ha deciso: rischio, paletti, duello, ev-regime.</summary>
public string Policy { get; set; } = "";
/// <summary>Chiave del contesto per il bandit, se il modello ha parlato.</summary>
public string? ContextKey { get; set; }
/// <summary>Cosa avrebbe fatto il bandit Thompson sullo stesso istante.</summary>
public string? AltAction { get; set; }
/// <summary>La probabilità estratta dal bandit.</summary>
public double? AltP { get; set; }
}
public class BankrollCheckResult
{
public bool CanBid { get; set; }
public string? Reason { get; set; }
}
public class SessionStats
{
public int TotalBids { get; set; }
public TimeSpan SessionDuration { get; set; }
}
}