Tutto ciò che l'applicazione registra sta ora in due file SQLite in %LocalAppData%\AutoBidder\Database: autobidder.sqlite per le osservazioni e esercizio.sqlite per i documenti d'esercizio (aste nel monitor, prodotti, promozioni, modelli appresi con prefisso ml/) e per i registri applicativo e del riscatto puntate. Il motore comune è SqliteDatabase; i file JSON e la cartella Apprendimento delle versioni precedenti vengono importati la prima volta e lasciati dove sono. La cartella si cambia dalle Impostazioni: al salvataggio si chiede se spostare i file, e si riavvia. Le cartelle Dati/Statistiche/Registri, i registri su file, le misure dell'anticipo e le impostazioni morte (LogBids, AutoApplyProductDefaults, NewAuctionLimitsPriority…) non esistono più. Il kill-switch se ne va: al suo posto un solo interruttore in barra, accanto ad Avvia/Osserva/Ferma, che spegne l'apprendimento. Spento, il motore punta solo entro i limiti dell'utente (prezzo, puntate, budget, fascia oraria, rischio) con l'anticipo fisso: niente valore atteso, regime, duello, bandit, anticipo adattivo. Le aste vengono comunque registrate e studiate. Tre difetti visti osservando il motore dal vivo per un'ora, in Osserva: - la copertura «Compralo Ora» con V lasciato al listino azzerava la perdita coperta e il motore diceva BID a ogni scadenza con P dell'1%: ora vale solo per i prodotti con «Valore reale €» scritto, altrimenti la puntata si conta persa e P decide; - la chiusura di un'asta arrivava due volte (fine, poi rimozione dal monitor) e di nuovo al riavvio: modello, profilo, bandit e statistiche per prodotto contavano ogni asta due volte. Il monitor la comunica una volta sola, l'apprendimento salta le aste già apprese, e le schede prodotto vengono ricostruite una volta dallo storico; - la pagina delle ricompense veniva scambiata per la pagina di accesso perché porta un collegamento a /login.php: prima si guarda se è la pagina delle ricompense. Interfaccia: la scheda Esporta e tutto il suo codice sono tolti; le esportazioni chiedono sempre dove salvare. Prodotti, Storico e Apprendimento hanno barre a sole icone con tooltip che dicono esattamente cosa succede, colorate come il monitor, con i nuovi pulsanti di pulizia (spegni tutte le stelline, togli i non seguiti, pulizia completa; seleziona tutte / elimina le selezionate / svuota lo storico). Impostazioni: sezione Database, Manutenzione con «Elimina tutti i dati (tranne la login)» e «Impostazioni di fabbrica», esportazione dei registri in testo. Modifiche.txt, il prompt ormai realizzato, esce dal repository. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
173 lines
5.6 KiB
C#
173 lines
5.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using AutoBidder.Models;
|
|
using AutoBidder.Utilities;
|
|
using Xunit;
|
|
|
|
namespace AutoBidder.Tests;
|
|
|
|
/// <summary>
|
|
/// I filtri decidono cosa finisce nel file da analizzare. Un filtro che lascia passare
|
|
/// un'asta sbagliata non si nota — si notano invece le conclusioni sbagliate che se ne
|
|
/// traggono un mese dopo, quando non c'è più modo di capire da dove venivano.
|
|
/// </summary>
|
|
public class AuctionDetailRecordTests
|
|
{
|
|
private static AuctionDetailRecord Record(
|
|
string id = "1",
|
|
string name = "Cuffie Sony",
|
|
double finalPrice = 5,
|
|
int myBids = 3,
|
|
bool won = false,
|
|
bool fromStart = true,
|
|
bool toEnd = true,
|
|
DateTime? endedAt = null) => new()
|
|
{
|
|
AuctionId = id,
|
|
Name = name,
|
|
ProductKey = "cuffie-sony",
|
|
FinalPrice = finalPrice,
|
|
MyBids = myBids,
|
|
WonByMe = won,
|
|
ObservedFromStart = fromStart,
|
|
ObservedToEnd = toEnd,
|
|
EndedAt = endedAt ?? DateTime.Now.AddDays(-1)
|
|
};
|
|
|
|
[Fact]
|
|
public void Unasta_completa_e_quella_vista_dallinizio_alla_fine()
|
|
{
|
|
Assert.True(Record(fromStart: true, toEnd: true).IsComplete);
|
|
Assert.False(Record(fromStart: false, toEnd: true).IsComplete);
|
|
Assert.False(Record(fromStart: true, toEnd: false).IsComplete);
|
|
}
|
|
|
|
[Fact]
|
|
public void Il_costo_reale_comprende_le_puntate_spese_e_la_spedizione()
|
|
{
|
|
// È il numero che dice se conveniva davvero: il solo prezzo finale non lo dice.
|
|
var record = Record(finalPrice: 5, myBids: 10);
|
|
record.ShippingCost = 6.90;
|
|
|
|
Assert.Equal(13.90, record.TotalCostIfWon(0.20), 2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Il_rapporto_col_valore_del_prodotto_manca_se_il_valore_non_si_conosce()
|
|
{
|
|
// Meglio nessun numero che uno inventato: un rapporto calcolato su un valore
|
|
// assente sarebbe indistinguibile da un affare vero.
|
|
Assert.Null(Record().FinalPriceRatio);
|
|
|
|
var withValue = Record(finalPrice: 30);
|
|
withValue.BuyNowPrice = 300;
|
|
|
|
Assert.Equal(0.1, withValue.FinalPriceRatio!.Value, 3);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// La scheda prodotto tiene i numeri su cui si decidono i limiti. Devono restare coerenti
|
|
/// mentre si accumulano, perché nessuno li ricalcola: si sommano una volta sola, quando
|
|
/// l'asta finisce.
|
|
/// </summary>
|
|
public class ProductStatsStoreTests
|
|
{
|
|
private static CompletedAuctionRecord Completed(
|
|
string key,
|
|
double price,
|
|
int myBids,
|
|
bool won,
|
|
DateTime? endedAt = null) => new()
|
|
{
|
|
AuctionId = Guid.NewGuid().ToString("N"),
|
|
Name = "Prodotto di prova",
|
|
ProductKey = key,
|
|
FinalPrice = price,
|
|
MyBidsUsed = myBids,
|
|
WonByMe = won,
|
|
EndedAt = endedAt ?? DateTime.Now
|
|
};
|
|
|
|
private static string NewKey() => "p-" + Guid.NewGuid().ToString("N")[..8];
|
|
|
|
[Fact]
|
|
public void La_prima_asta_crea_la_scheda()
|
|
{
|
|
var key = NewKey();
|
|
ProductStatsStore.RecordCompleted(Completed(key, 5, 2, won: false), null);
|
|
|
|
var card = ProductStatsStore.Find(key);
|
|
|
|
Assert.NotNull(card);
|
|
Assert.Equal(1, card!.AuctionsCompleted);
|
|
Assert.Equal(5, card.LastFinalPrice);
|
|
}
|
|
|
|
[Fact]
|
|
public void Le_medie_si_accumulano_senza_rileggere_lo_storico()
|
|
{
|
|
var key = NewKey();
|
|
|
|
ProductStatsStore.RecordCompleted(Completed(key, 4, 1, won: false), null);
|
|
ProductStatsStore.RecordCompleted(Completed(key, 8, 3, won: true), null);
|
|
|
|
var card = ProductStatsStore.Find(key)!;
|
|
|
|
Assert.Equal(2, card.AuctionsCompleted);
|
|
Assert.Equal(6, card.AverageFinalPrice);
|
|
Assert.Equal(4, card.MinFinalPrice);
|
|
Assert.Equal(8, card.MaxFinalPrice);
|
|
Assert.Equal(4, card.MyBidsTotal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Le_aste_solo_osservate_non_abbassano_la_percentuale_di_vittorie()
|
|
{
|
|
// Contarle come sconfitte racconterebbe un fallimento mai avvenuto: su quelle
|
|
// non si è mai puntato.
|
|
var key = NewKey();
|
|
|
|
ProductStatsStore.RecordCompleted(Completed(key, 5, myBids: 0, won: false), null);
|
|
ProductStatsStore.RecordCompleted(Completed(key, 6, myBids: 4, won: true), null);
|
|
|
|
var card = ProductStatsStore.Find(key)!;
|
|
|
|
Assert.Equal(2, card.AuctionsCompleted);
|
|
Assert.Equal(1, card.AuctionsWithMyBids);
|
|
Assert.Equal(100, card.WinRatePercent);
|
|
}
|
|
|
|
// Il prezzo massimo consigliato non e' piu' una proprieta' della scheda prodotto:
|
|
// dipende dalla distribuzione delle chiusure, che la scheda non conserva. Vive in
|
|
// PriceAdvisor, ed e' provato in PriceAdvisorTests.
|
|
|
|
[Fact]
|
|
public void Le_osservazioni_complete_si_contano_a_parte()
|
|
{
|
|
var key = NewKey();
|
|
|
|
var complete = new AuctionDetailRecord { ObservedFromStart = true, ObservedToEnd = true, ObservedMinutes = 30 };
|
|
var partial = new AuctionDetailRecord { ObservedFromStart = false, ObservedToEnd = true, ObservedMinutes = 10 };
|
|
|
|
ProductStatsStore.RecordCompleted(Completed(key, 5, 1, false), complete);
|
|
ProductStatsStore.RecordCompleted(Completed(key, 5, 1, false), partial);
|
|
|
|
var card = ProductStatsStore.Find(key)!;
|
|
|
|
Assert.Equal(2, card.AuctionsCompleted);
|
|
Assert.Equal(1, card.CompleteObservations);
|
|
Assert.Equal(20, card.AverageObservedMinutes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_prodotto_mai_visto_non_ha_scheda()
|
|
{
|
|
Assert.Null(ProductStatsStore.Find(NewKey()));
|
|
Assert.Null(ProductStatsStore.Find(""));
|
|
}
|
|
}
|