Una scheda nuova nella barra laterale, fra Esporta e Impostazioni, che legge direttamente dal servizio e si aggiorna da sola ogni cinque secondi mentre e' visibile: nessun altro pezzo dell'applicazione sa che esiste. Mostra: lo stato (aste apprese, esempi visti, aste nel profilo, fattore di calibrazione, soglia per decidere, se lo studio dei dossier e' in corso); i pesi dal piu' forte con il verso dell'effetto, perche' le variabili sono a fasce e il modello e' lineare proprio per poterli leggere; le ultime decisioni date al motore, con probabilita', valore atteso ed esito (fermata, passa, o solo parere se il modello non aveva ancora appreso abbastanza); il profilo per prodotto e fascia oraria; e l'ultima valutazione. La valutazione si lancia dalla scheda, in sottofondo e annullabile. La logica e' uscita dal test ed e' entrata in LearningEvaluator, condivisa fra il target da riga di comando e la scheda: i numeri che vede l'utente sono gli stessi che vede chi sviluppa. "Ricomincia da capo" butta modello, profilo ed elenco dei dossier letti e ristudia tutto l'archivio, con una conferma prima. Le decisioni si tengono in una coda corta; lo stesso giro di un'asta ne produrrebbe molte uguali di fila, quindi si registra solo il cambiamento. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
548 lines
23 KiB
C#
548 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using AutoBidder.Models;
|
|
using AutoBidder.Utilities;
|
|
|
|
namespace AutoBidder.Ml
|
|
{
|
|
/// <summary>
|
|
/// L'apprendimento, sempre acceso: impara da ogni asta che chiude e risponde al motore
|
|
/// quando deve decidere.
|
|
///
|
|
/// <para><b>Due cose imparate, per due domande diverse.</b> Il <i>modello delle
|
|
/// puntate</i> risponde a «se punto adesso, qualcuno risponderà?»: una regressione
|
|
/// logistica su ciò che si vede nell'istante della puntata. Il <i>profilo delle aste</i>
|
|
/// risponde a «quanto costa di solito vincere questo prodotto a quest'ora?»: medie per
|
|
/// prodotto, fascia oraria e tipo di giorno, con restringimento dove i dati scarseggiano.</para>
|
|
///
|
|
/// <para><b>Da dove impara.</b> Dai dossier. Ogni puntata di ogni asta chiusa è un
|
|
/// esempio: rimasta senza risposta oppure no. Non dalle nostre vittorie, che sono
|
|
/// diciannove in duemila aste: un modello addestrato su quelle non sarebbe un modello.
|
|
/// Il comportamento avversario non dipende da chi ha puntato, e questo rende
|
|
/// utilizzabili i milioni di puntate altrui.</para>
|
|
///
|
|
/// <para><b>Il primo addestramento</b> è incrementale e in sottofondo: i dossier sono
|
|
/// migliaia e pesano gigabyte, e si leggono un po' per avvio, dal più recente, con un
|
|
/// tempo massimo per sessione. Quello che è già stato letto non si rilegge: un elenco
|
|
/// dei file appresi lo tiene a mente. Il profilo delle aste invece parte subito dallo
|
|
/// storico compatto, che è piccolo e ha le puntate del vincitore già verificate.</para>
|
|
///
|
|
/// <para><b>Cosa NON può dire.</b> Il modello impara come finiscono le aste senza di
|
|
/// noi. La nostra puntata cambia il gioco: chi stava per smettere può rispondere.
|
|
/// Nessun dato storico dice questo. Il modello dà la stima migliore possibile di
|
|
/// quando gli altri sono esausti, ed è quanto si può chiedere ai dati.</para>
|
|
/// </summary>
|
|
public static class LearningService
|
|
{
|
|
private static readonly object Sync = new();
|
|
|
|
// I salvataggi hanno un lucchetto tutto loro: all'avvio più aste concluse nel
|
|
// frattempo vengono chiuse insieme, ognuna su un proprio thread, e senza questo
|
|
// scrivevano lo stesso file nello stesso istante — "file in uso da un altro
|
|
// processo", che in realtà eravamo noi.
|
|
private static readonly object SaveSync = new();
|
|
|
|
private static OnlineLogit _model = new(BidFeatures.Size);
|
|
private static AuctionProfileStats _profile = new();
|
|
private static readonly HashSet<string> _ingested = new(StringComparer.OrdinalIgnoreCase);
|
|
private static int _auctionsLearned;
|
|
private static bool _loaded;
|
|
private static volatile bool _bootstrapRunning;
|
|
|
|
/// <summary>Righe di registro sull'apprendimento: chi si aggancia le scrive dove vuole.</summary>
|
|
public static event Action<string>? OnLog;
|
|
|
|
// ── Percorsi ─────────────────────────────────────────────────────
|
|
|
|
private static string Folder => Path.Combine(AppPaths.StatsFolder, "Apprendimento");
|
|
private static string ModelFile => Path.Combine(Folder, "modello-puntate.json");
|
|
private static string ProfileFile => Path.Combine(Folder, "profilo-aste.json");
|
|
private static string ManifestFile => Path.Combine(Folder, "dossier-appresi.txt");
|
|
|
|
// ── Stato, per l'interfaccia e il registro ───────────────────────
|
|
|
|
public static long ModelUpdates { get { lock (Sync) return _model.Updates; } }
|
|
public static double CalibrationFactor { get { lock (Sync) return _model.CalibrationFactor; } }
|
|
public static int AuctionsLearned { get { lock (Sync) return _auctionsLearned; } }
|
|
public static long ProfileAuctions { get { lock (Sync) return _profile.AuctionsObserved; } }
|
|
public static bool BootstrapRunning => _bootstrapRunning;
|
|
|
|
/// <summary>
|
|
/// Abbastanza aste apprese perché il cancello sul valore atteso possa fermare una
|
|
/// puntata. Prima di questa soglia il modello parla, ma non decide.
|
|
/// </summary>
|
|
public static bool IsReady(AppSettings settings) =>
|
|
AuctionsLearned >= Math.Max(1, settings.LearningMinAuctions);
|
|
|
|
/// <summary>I pesi con i loro nomi, ordinati per peso: per guardare cosa ha imparato.</summary>
|
|
public static IReadOnlyList<(string Nome, double Peso)> Pesi()
|
|
{
|
|
lock (Sync)
|
|
{
|
|
return BidFeatures.Names
|
|
.Select((n, i) => (n, _model.Weights[i]))
|
|
.OrderByDescending(t => Math.Abs(t.Item2))
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>Il profilo per prodotto e fascia, dalla combinazione più numerosa.</summary>
|
|
public static IReadOnlyList<AuctionProfileStats.Row> ProfileRows(int max = 200)
|
|
{
|
|
lock (Sync) return _profile.Rows(max);
|
|
}
|
|
|
|
// ── Ultime decisioni, per la scheda ──────────────────────────────
|
|
//
|
|
// Il motore chiede al modello a ogni giro utile, e la risposta finisce nel
|
|
// registro dell'asta. Qui se ne tiene una coda corta per vederle tutte insieme:
|
|
// e' l'unico modo di capire a colpo d'occhio se il cancello sta fermando troppo o
|
|
// troppo poco.
|
|
|
|
/// <summary>Una risposta data al motore.</summary>
|
|
public readonly record struct Decision(
|
|
DateTime At, string Auction, double Price, double Probability, double ExpectedValue, bool Blocked, bool Ready);
|
|
|
|
private const int MaxDecisions = 300;
|
|
private static readonly Queue<Decision> _decisions = new();
|
|
|
|
public static void RecordDecision(string auction, double price, double probability, double expectedValue, bool blocked, bool ready)
|
|
{
|
|
lock (_decisions)
|
|
{
|
|
// Lo stesso giro dell'asta produce molte risposte uguali di fila: si tiene
|
|
// solo il cambiamento, altrimenti la coda si riempie di una sola asta.
|
|
if (_decisions.Count > 0)
|
|
{
|
|
var last = _decisions.Last();
|
|
if (last.Auction == auction && last.Blocked == blocked &&
|
|
Math.Abs(last.Price - price) < 0.005 && (DateTime.Now - last.At).TotalSeconds < 2)
|
|
return;
|
|
}
|
|
|
|
_decisions.Enqueue(new Decision(DateTime.Now, auction, price, probability, expectedValue, blocked, ready));
|
|
while (_decisions.Count > MaxDecisions) _decisions.Dequeue();
|
|
}
|
|
}
|
|
|
|
public static IReadOnlyList<Decision> RecentDecisions()
|
|
{
|
|
lock (_decisions) return _decisions.Reverse().ToList();
|
|
}
|
|
|
|
// ── Valutazione dall'applicazione ────────────────────────────────
|
|
|
|
private static string EvaluationFile => Path.Combine(Folder, "valutazione.txt");
|
|
|
|
/// <summary>L'ultimo rapporto di valutazione salvato, se c'è.</summary>
|
|
public static string? LastEvaluation()
|
|
{
|
|
try { return File.Exists(EvaluationFile) ? File.ReadAllText(EvaluationFile) : null; }
|
|
catch { return null; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Valuta il modello sui dossier di questa macchina, in sottofondo, e salva il
|
|
/// rapporto. È la stessa valutazione del target da riga di comando.
|
|
/// </summary>
|
|
public static Task<LearningEvaluator.Report> EvaluateAsync(int maxFiles, Action<string>? progress, System.Threading.CancellationToken ct)
|
|
{
|
|
return Task.Run(() =>
|
|
{
|
|
var me = AuctionDossier.CurrentUsername;
|
|
var report = LearningEvaluator.Run(AppPaths.AuctionLogFolder, maxFiles, string.IsNullOrEmpty(me) ? null : me, progress, ct);
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Folder);
|
|
lock (SaveSync) File.WriteAllText(EvaluationFile, $"Valutazione del {DateTime.Now:dd/MM/yyyy HH:mm}\n\n" + report.Text);
|
|
}
|
|
catch { /* il rapporto e' comunque restituito */ }
|
|
|
|
return report;
|
|
}, ct);
|
|
}
|
|
|
|
// ── Ricominciare da capo ─────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Butta modello, profilo ed elenco dei dossier letti, e ricomincia a studiare
|
|
/// dall'archivio. Serve quando si cambiano le variabili o si sospetta che il
|
|
/// modello abbia imparato da dati sbagliati. Non tocca i dossier.
|
|
/// </summary>
|
|
public static Task RetrainFromScratchAsync(AppSettings settings)
|
|
{
|
|
lock (Sync)
|
|
{
|
|
_model = new OnlineLogit(BidFeatures.Size);
|
|
_profile = new AuctionProfileStats();
|
|
_ingested.Clear();
|
|
_auctionsLearned = 0;
|
|
}
|
|
|
|
lock (_decisions) _decisions.Clear();
|
|
|
|
try
|
|
{
|
|
lock (SaveSync)
|
|
{
|
|
foreach (var f in new[] { ModelFile, ProfileFile, ManifestFile })
|
|
if (File.Exists(f)) File.Delete(f);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Archivio non cancellato del tutto: {ex.Message}");
|
|
}
|
|
|
|
OnLog?.Invoke("[APPRENDIMENTO] Ricomincio da capo: profilo dallo storico, poi tutti i dossier.");
|
|
BootstrapProfileFromHistory();
|
|
|
|
// Un riaddestramento voluto ha diritto a tutto il tempo che serve.
|
|
var generose = new AppSettings { LearningBootstrapSecondsPerStart = Math.Max(600, settings.LearningBootstrapSecondsPerStart) };
|
|
return Task.Run(() => BootstrapAsync(generose));
|
|
}
|
|
|
|
// ── Avvio ────────────────────────────────────────────────────────
|
|
|
|
public static void Start(AppSettings settings)
|
|
{
|
|
Load();
|
|
BootstrapProfileFromHistory();
|
|
_ = Task.Run(() => BootstrapAsync(settings));
|
|
}
|
|
|
|
private static void Load()
|
|
{
|
|
lock (Sync)
|
|
{
|
|
if (_loaded) return;
|
|
_loaded = true;
|
|
|
|
try
|
|
{
|
|
if (File.Exists(ModelFile))
|
|
_model = OnlineLogit.FromJson(File.ReadAllText(ModelFile), BidFeatures.Size);
|
|
if (File.Exists(ProfileFile))
|
|
_profile = AuctionProfileStats.FromJson(File.ReadAllText(ProfileFile));
|
|
if (File.Exists(ManifestFile))
|
|
{
|
|
foreach (var line in File.ReadLines(ManifestFile))
|
|
{
|
|
var l = line.Trim();
|
|
if (l.Length == 0) continue;
|
|
if (l.StartsWith("#", StringComparison.Ordinal))
|
|
{
|
|
if (l.StartsWith("#appresi=", StringComparison.Ordinal) &&
|
|
int.TryParse(l["#appresi=".Length..], out var n)) _auctionsLearned = n;
|
|
continue;
|
|
}
|
|
_ingested.Add(l);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Archivio illeggibile, si riparte da zero: {ex.Message}");
|
|
_model = new OnlineLogit(BidFeatures.Size);
|
|
_profile = new AuctionProfileStats();
|
|
_ingested.Clear();
|
|
_auctionsLearned = 0;
|
|
}
|
|
}
|
|
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Modello: {ModelUpdates:N0} aggiornamenti da {AuctionsLearned:N0} aste; " +
|
|
$"profilo: {ProfileAuctions:N0} aste; dossier già letti: {_ingested.Count:N0}");
|
|
}
|
|
|
|
private static void Save()
|
|
{
|
|
try
|
|
{
|
|
string model, profile;
|
|
lock (Sync)
|
|
{
|
|
model = _model.ToJson();
|
|
profile = _profile.ToJson();
|
|
}
|
|
|
|
lock (SaveSync)
|
|
{
|
|
Directory.CreateDirectory(Folder);
|
|
File.WriteAllText(ModelFile, model);
|
|
File.WriteAllText(ProfileFile, profile);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Salvataggio non riuscito: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static void SaveManifest()
|
|
{
|
|
try
|
|
{
|
|
string[] lines;
|
|
lock (Sync)
|
|
{
|
|
lines = new[] { "#appresi=" + _auctionsLearned }
|
|
.Concat(_ingested.OrderBy(x => x, StringComparer.Ordinal))
|
|
.ToArray();
|
|
}
|
|
|
|
lock (SaveSync)
|
|
{
|
|
Directory.CreateDirectory(Folder);
|
|
File.WriteAllLines(ManifestFile, lines);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Elenco dei dossier letti non salvato: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Il profilo parte dallo storico compatto: piccolo, immediato, e con le puntate
|
|
/// del vincitore già passate dal controllo di coerenza. Solo se il profilo è vuoto,
|
|
/// perché lo storico è la stessa fonte che poi lo alimenta asta per asta.
|
|
/// </summary>
|
|
private static void BootstrapProfileFromHistory()
|
|
{
|
|
lock (Sync) { if (_profile.AuctionsObserved > 0) return; }
|
|
|
|
try
|
|
{
|
|
var records = CompletedAuctionsStore.LoadAll();
|
|
var fresh = new AuctionProfileStats();
|
|
|
|
foreach (var r in records)
|
|
fresh.Observe(r.ProductKey, r.EndedAt.ToLocalTime(),
|
|
AuctionIntegrity.TrustedWinnerBids(r), Ratio(r.FinalPrice, r.BuyNowPrice));
|
|
|
|
lock (Sync) _profile = fresh;
|
|
Save();
|
|
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Profilo delle aste costruito dallo storico: {records.Count:N0} aste");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Profilo dallo storico non riuscito: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Legge i dossier non ancora appresi, dal più recente, finché c'è tempo. Il tempo è
|
|
/// per avvio: la prima volta ne servono diversi, poi resta solo l'asta appena chiusa.
|
|
/// </summary>
|
|
private static async Task BootstrapAsync(AppSettings settings)
|
|
{
|
|
if (_bootstrapRunning) return;
|
|
_bootstrapRunning = true;
|
|
|
|
try
|
|
{
|
|
await Task.Delay(3000).ConfigureAwait(false); // prima l'avvio, poi lo studio
|
|
|
|
var folder = AppPaths.AuctionLogFolder;
|
|
if (!Directory.Exists(folder)) return;
|
|
|
|
string[] pending;
|
|
lock (Sync)
|
|
{
|
|
pending = Directory.GetFiles(folder, "*.jsonl")
|
|
.Where(f => !_ingested.Contains(Path.GetFileName(f)))
|
|
.OrderByDescending(Path.GetFileName, StringComparer.Ordinal)
|
|
.ToArray();
|
|
}
|
|
|
|
if (pending.Length == 0) return;
|
|
|
|
var budget = TimeSpan.FromSeconds(Math.Max(10, settings.LearningBootstrapSecondsPerStart));
|
|
var sw = Stopwatch.StartNew();
|
|
var done = 0;
|
|
var usable = 0;
|
|
|
|
OnLog?.Invoke($"[APPRENDIMENTO] {pending.Length:N0} dossier da leggere, al massimo {budget.TotalSeconds:N0} s per questo avvio");
|
|
|
|
foreach (var file in pending)
|
|
{
|
|
if (sw.Elapsed > budget) break;
|
|
|
|
if (IngestFile(file)) usable++;
|
|
done++;
|
|
|
|
if (done % 100 == 0)
|
|
{
|
|
Save();
|
|
SaveManifest();
|
|
OnLog?.Invoke($"[APPRENDIMENTO] {done:N0} dossier letti, {usable:N0} utilizzabili, {ModelUpdates:N0} aggiornamenti");
|
|
}
|
|
|
|
// Un respiro ogni file: il motore delle aste ha la precedenza.
|
|
await Task.Yield();
|
|
}
|
|
|
|
Save();
|
|
SaveManifest();
|
|
|
|
var remaining = pending.Length - done;
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Letti {done:N0} dossier ({usable:N0} utilizzabili) in {sw.Elapsed.TotalSeconds:N0} s; " +
|
|
(remaining > 0 ? $"ne restano {remaining:N0} per i prossimi avvii." : "archivio completo."));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Studio dei dossier interrotto: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
_bootstrapRunning = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Impara da un dossier. Restituisce true se era utilizzabile. Un file letto si
|
|
/// segna come letto anche se non era utilizzabile: rileggerlo non lo renderebbe tale.
|
|
/// </summary>
|
|
private static bool IngestFile(string path)
|
|
{
|
|
var name = Path.GetFileName(path);
|
|
var session = DossierTrainingReader.ReadFile(path);
|
|
|
|
lock (Sync)
|
|
{
|
|
_ingested.Add(name);
|
|
if (session == null || !session.IsUsable) return false;
|
|
|
|
foreach (var (ctx, unanswered, _) in DossierTrainingReader.Examples(session))
|
|
_model.Update(BidFeatures.Build(ctx), unanswered);
|
|
|
|
_auctionsLearned++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ── Chiusura di un'asta: si impara subito ────────────────────────
|
|
|
|
/// <summary>
|
|
/// Da chiamare quando un'asta è chiusa e il suo dossier è stato scritto. Aggiorna
|
|
/// il profilo con lo storico appena registrato e il modello con le puntate del
|
|
/// dossier, poi salva: la prossima decisione le vede già.
|
|
/// </summary>
|
|
public static void OnAuctionClosed(CompletedAuctionRecord record, string? dossierPath)
|
|
{
|
|
if (record == null) return;
|
|
|
|
try
|
|
{
|
|
lock (Sync)
|
|
{
|
|
_profile.Observe(record.ProductKey, record.EndedAt.ToLocalTime(),
|
|
AuctionIntegrity.TrustedWinnerBids(record), Ratio(record.FinalPrice, record.BuyNowPrice));
|
|
}
|
|
|
|
var learned = false;
|
|
if (!string.IsNullOrWhiteSpace(dossierPath) && File.Exists(dossierPath))
|
|
{
|
|
// Il dossier è appena stato svuotato su disco da Close: si può leggere.
|
|
learned = IngestFile(dossierPath);
|
|
}
|
|
|
|
Save();
|
|
SaveManifest();
|
|
|
|
OnLog?.Invoke(learned
|
|
? $"[APPRENDIMENTO] '{record.Name}': appresa ({ModelUpdates:N0} aggiornamenti, {AuctionsLearned:N0} aste)"
|
|
: $"[APPRENDIMENTO] '{record.Name}': profilo aggiornato, dossier non utilizzabile per il modello");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog?.Invoke($"[APPRENDIMENTO] Errore su '{record.Name}': {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// ── Domande dal motore ───────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Se puntassi adesso, con che probabilità nessuno risponderebbe? Null quando non
|
|
/// c'è abbastanza contesto (nessuna puntata recente vista).
|
|
/// </summary>
|
|
public static double? PredictUnanswered(AuctionInfo auction, AuctionState state, DateTime nowLocal, string? myUsername)
|
|
{
|
|
if (auction == null || state == null) return null;
|
|
|
|
var recent = auction.SnapshotRecentBids(20); // dalla più recente
|
|
if (recent.Count == 0) return null;
|
|
|
|
var last = recent[0];
|
|
var gap = state.ServerUnixSeconds > 0 && last.Timestamp > 0
|
|
? state.ServerUnixSeconds - last.Timestamp
|
|
: -1;
|
|
|
|
// Quota di autopuntate: solo sui tipi dichiarati dal server.
|
|
int declared = 0, auto = 0;
|
|
foreach (var b in recent.Take(10))
|
|
{
|
|
if (b.IsAuto) { auto++; declared++; }
|
|
else if (b.IsManual) declared++;
|
|
}
|
|
var autoShare = declared == 0 ? -1 : (double)auto / declared;
|
|
|
|
var distinct = recent.Select(b => b.Username).Distinct(StringComparer.OrdinalIgnoreCase).Count();
|
|
|
|
var sinceManual = -1;
|
|
for (var i = 0; i < recent.Count; i++)
|
|
{
|
|
if (recent[i].IsManual) { sinceManual = i; break; }
|
|
if (!recent[i].IsAuto) break; // tipo ignoto: non si conta
|
|
}
|
|
|
|
var ctx = new BidContext(
|
|
CycleSeconds: gap,
|
|
AutoShareRecent: autoShare,
|
|
DistinctBiddersRecent: Math.Max(1, distinct),
|
|
PriceRatio: auction.BuyNowPrice is > 0 ? state.Price / auction.BuyNowPrice.Value : null,
|
|
Hour: nowLocal.Hour,
|
|
Day: nowLocal.DayOfWeek,
|
|
OpenHour: auction.AddedAt.ToLocalTime().Hour,
|
|
IsAutoBid: false,
|
|
CyclesSinceManual: sinceManual,
|
|
AuctionDepthCents: (int)Math.Round(state.Price * 100),
|
|
SameBidderAsTwoAgo: recent.Count >= 2 && !string.IsNullOrEmpty(myUsername) &&
|
|
string.Equals(recent[1].Username, myUsername, StringComparison.OrdinalIgnoreCase),
|
|
ProductKey: ProductKeyHelper.GenerateProductKey(auction.Name));
|
|
|
|
var x = BidFeatures.Build(ctx);
|
|
lock (Sync) return _model.PredictCalibrated(x);
|
|
}
|
|
|
|
/// <summary>Cosa aspettarsi da un'asta di questo prodotto se chiudesse a quest'ora.</summary>
|
|
public static AuctionProfileStats.Estimate? Expect(string? productKey, DateTime whenLocal)
|
|
{
|
|
lock (Sync) return _profile.Expect(productKey, whenLocal);
|
|
}
|
|
|
|
private static double? Ratio(double finalPrice, double? buyNow) =>
|
|
buyNow is > 0 && finalPrice > 0 ? finalPrice / buyNow.Value : null;
|
|
|
|
/// <summary>Solo per i test: stato pulito, senza toccare il disco.</summary>
|
|
public static void ResetForTests()
|
|
{
|
|
lock (Sync)
|
|
{
|
|
_model = new OnlineLogit(BidFeatures.Size);
|
|
_profile = new AuctionProfileStats();
|
|
_ingested.Clear();
|
|
_auctionsLearned = 0;
|
|
_loaded = true;
|
|
}
|
|
}
|
|
}
|
|
}
|