diff --git a/Mimante/Models/AuctionInfo.cs b/Mimante/Models/AuctionInfo.cs index 58c7e00..1e744a2 100644 --- a/Mimante/Models/AuctionInfo.cs +++ b/Mimante/Models/AuctionInfo.cs @@ -703,6 +703,13 @@ namespace AutoBidder.Models /// Override: abilita/disabilita soft retreat per questa asta /// public bool? SoftRetreatEnabledOverride { get; set; } + + /// + /// Override per questa asta della sospensione a fascia oraria. + /// null = come dice il prodotto, e se il prodotto tace come dicono le + /// impostazioni globali. Vedi . + /// + public bool? QuietHoursEnabledOverride { get; set; } /// /// Override: limite puntate per questa asta diff --git a/Mimante/Models/WatchedProduct.cs b/Mimante/Models/WatchedProduct.cs index 2cbc5d1..110a56d 100644 --- a/Mimante/Models/WatchedProduct.cs +++ b/Mimante/Models/WatchedProduct.cs @@ -32,6 +32,13 @@ namespace AutoBidder.Models /// Prezzo minimo sotto il quale non puntare. null = predefinito. public double? MinPrice { get; set; } + /// + /// Sospendere le puntate nella fascia oraria. null = come le impostazioni + /// generali. Un'asta può a sua volta scavalcare questo valore. + /// Vedi . + /// + public bool? QuietHoursEnabled { get; set; } + /// Prezzo oltre il quale lasciar perdere l'asta. null = predefinito. public double? MaxPrice { get; set; } diff --git a/Mimante/Services/BidStrategyService.cs b/Mimante/Services/BidStrategyService.cs index 26bcf44..9274103 100644 --- a/Mimante/Services/BidStrategyService.cs +++ b/Mimante/Services/BidStrategyService.cs @@ -190,6 +190,26 @@ namespace AutoBidder.Services return decision; } + // 0-bis. FASCIA ORARIA SOSPESA. + // + // L'asta NON viene fermata: resta Attiva, continua a essere seguita e + // riprende a puntare da sola alla fine della fascia. È voluto — le aste + // lunghe attraversano la fascia, e fermarle davvero significherebbe + // perderle invece che risparmiare. + // + // Su 6060 aste concluse: chiusura mediana al 4,6% del valore alle 12 e al + // 5,3% fra le 10 e le 13, contro l'8,6% a mezzanotte e il 9,7% alle 9, con + // 26-29 puntate per vincere invece di 21-24. Quasi il doppio del costo per + // lo stesso oggetto. + if (QuietHoursApply(auction, settings) && + Utilities.BiddingHours.IsQuiet(DateTime.Now, settings.QuietHoursStart, settings.QuietHoursEnd)) + { + decision.ShouldBid = false; + decision.Reason = Utilities.BiddingHours.Spiegazione( + settings.QuietHoursStart, settings.QuietHoursEnd); + return decision; + } + // ? RIMOSSO: Entry Point - Era sbagliato! // I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI. // Se l'utente imposta MaxPrice=2�, vuole puntare FINO A 2�, non fino al 70%! @@ -486,6 +506,26 @@ namespace AutoBidder.Services /// /// Registra una puntata effettuata (per tracking) /// + /// + /// 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. Un null non + /// significa "no", significa "non ho un'opinione": è la differenza che permette di + /// spegnere il filtro su una singola asta senza toccare il resto. + /// + public static bool QuietHoursApply(AuctionInfo auction, AppSettings settings) + { + if (auction?.QuietHoursEnabledOverride is { } perAsta) return perAsta; + + // Le aste del monitor non portano con sé l'id prodotto di Bidoo: la scheda si + // ritrova dal nome, che è la via prevista da WatchedProductsStore. + var scheda = WatchedProductsStore.FindByName(auction?.Name); + if (scheda?.QuietHoursEnabled is { } perProdotto) return perProdotto; + + return settings.QuietHoursEnabled; + } + public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false) { auction.SessionBidCount++; diff --git a/Mimante/Tests/BiddingHoursTests.cs b/Mimante/Tests/BiddingHoursTests.cs new file mode 100644 index 0000000..0b27843 --- /dev/null +++ b/Mimante/Tests/BiddingHoursTests.cs @@ -0,0 +1,92 @@ +using System; +using AutoBidder.Utilities; +using Xunit; + +namespace AutoBidder.Tests; + +/// +/// Fascia oraria sospesa. +/// +/// La proprieta' da difendere e' che la fascia non fermi l'asta: salta la +/// puntata e basta, perche' le aste lunghe attraversano la fascia e fermarle davvero +/// significherebbe perderle. Qui si prova il calcolo della fascia; che lo stato resti +/// Attiva e' garantito da come la regola e' collegata, non da questo file. +/// +/// I numeri che motivano il predefinito 0-10: su 6060 aste concluse la chiusura +/// mediana e' il 4,6% del valore alle 12 e il 5,3% fra le 10 e le 13, contro l'8,6% a +/// mezzanotte e il 9,7% alle 9. +/// +public class BiddingHoursTests +{ + [Theory] + [InlineData(0, true)] // mezzanotte: la peggiore, 8,6% + [InlineData(3, true)] + [InlineData(9, true)] // ultima ora dentro: 9,7% + [InlineData(10, false)] // si riparte: 4,9% + [InlineData(12, false)] // la migliore: 4,6% + [InlineData(23, false)] + public void La_fascia_predefinita_va_da_mezzanotte_alle_dieci(int ora, bool sospesa) + { + Assert.Equal(sospesa, + BiddingHours.IsQuiet(ora, BiddingHours.DefaultStart, BiddingHours.DefaultEnd)); + } + + [Fact] + public void Inizio_incluso_e_fine_esclusa() + { + Assert.True(BiddingHours.IsQuiet(1, 1, 5)); + Assert.True(BiddingHours.IsQuiet(4, 1, 5)); + Assert.False(BiddingHours.IsQuiet(5, 1, 5)); + } + + [Theory] + [InlineData(22, true)] + [InlineData(23, true)] + [InlineData(0, true)] + [InlineData(5, true)] + [InlineData(6, false)] + [InlineData(12, false)] + public void Una_fascia_a_cavallo_di_mezzanotte_funziona(int ora, bool sospesa) + { + Assert.Equal(sospesa, BiddingHours.IsQuiet(ora, 22, 6)); + } + + [Fact] + public void Inizio_uguale_a_fine_significa_filtro_spento() + { + // L'alternativa sarebbe non puntare mai, che non e' quello che intende nessuno + // scrivendo due volte la stessa ora. + for (var ora = 0; ora < 24; ora++) + Assert.False(BiddingHours.IsQuiet(ora, 3, 3)); + } + + [Theory] + [InlineData(24, 0)] + [InlineData(25, 1)] + [InlineData(-1, 23)] + public void Le_ore_fuori_scala_si_riportano_dentro(int fuori, int atteso) + { + // Un valore scritto a mano nelle impostazioni non deve produrre una fascia che + // non sospende mai o che sospende sempre. + Assert.Equal(BiddingHours.IsQuiet(atteso, 0, 10), BiddingHours.IsQuiet(fuori, 0, 10)); + } + + [Fact] + public void La_spiegazione_dice_la_fascia_e_che_riprende_da_sola() + { + var testo = BiddingHours.Spiegazione(0, 10); + + Assert.Contains("00:00-10:00", testo); + Assert.Contains("resta attiva", testo); + } + + [Fact] + public void La_versione_con_data_usa_solo_l_ora() + { + var dentro = new DateTime(2026, 8, 7, 3, 45, 0); + var fuori = new DateTime(2026, 8, 7, 15, 45, 0); + + Assert.True(BiddingHours.IsQuiet(dentro, 0, 10)); + Assert.False(BiddingHours.IsQuiet(fuori, 0, 10)); + } +} diff --git a/Mimante/Utilities/BiddingHours.cs b/Mimante/Utilities/BiddingHours.cs new file mode 100644 index 0000000..4951f84 --- /dev/null +++ b/Mimante/Utilities/BiddingHours.cs @@ -0,0 +1,77 @@ +using System; + +namespace AutoBidder.Utilities +{ + /// + /// Fascia oraria in cui non si punta. + /// + /// Perché. La stessa asta non costa uguale a tutte le ore. Su 6060 aste + /// concluse con prezzo e valore noti, il prezzo di chiusura mediano in percentuale del + /// valore del prodotto è 4,6% alle 12 e 5,3% fra le 10 e le 13, ma sale a 8,6% a + /// mezzanotte e 9,7% alle 9. Le puntate che servono per vincere seguono la + /// stessa curva: 21-24 nelle ore buone, 26-29 in quelle cattive. Quasi il doppio del + /// costo per lo stesso oggetto. + /// + /// Il motivo è che di notte Bidoo sospende le puntate — restano poche aste in + /// corso e la concorrenza si concentra su quelle. Le ore di confine, mezzanotte e le + /// nove, sono le peggiori: c'è ancora qualcosa da giocare e ci sono tutti addosso. + /// + /// Cosa fa. Sospende le puntate, non il monitoraggio: l'asta resta in stato + /// Attiva, continua a essere seguita, e riprende a puntare da sola appena la fascia + /// finisce. È fatto apposta per le aste lunghe che attraversano la fascia — fermarle + /// davvero significherebbe perderle. + /// + /// Classe pura, l'ora arriva da fuori: così i test non dipendono da quando + /// vengono eseguiti. + /// + public static class BiddingHours + { + /// Prima ora della fascia sospesa, inclusa. Predefinito: mezzanotte. + public const int DefaultStart = 0; + + /// Prima ora in cui si torna a puntare. Predefinito: le 10. + public const int DefaultEnd = 10; + + /// + /// L'ora indicata cade nella fascia sospesa? + /// + /// Estremi: inizio incluso, fine esclusa — 0-10 significa "da mezzanotte fino + /// alle 9:59". Una fascia che scavalca la mezzanotte (22-6) funziona. Inizio uguale + /// a fine vuol dire fascia vuota, cioè filtro spento: è l'unica lettura sensata, + /// perché l'alternativa sarebbe non puntare mai. + /// + public static bool IsQuiet(int hour, int start, int end) + { + hour = Normalize(hour); + start = Normalize(start); + end = Normalize(end); + + if (start == end) return false; + + return start < end + ? hour >= start && hour < end + : hour >= start || hour < end; // a cavallo di mezzanotte + } + + /// Comodità: la fascia applicata a un istante. + public static bool IsQuiet(DateTime when, int start, int end) => + IsQuiet(when.Hour, start, end); + + /// + /// Quando riprende, per poterlo scrivere nel registro. Restituisce l'ora di ripresa + /// (0-23); chi legge sa già che è la prossima volta che scocca. + /// + public static int ResumesAt(int end) => Normalize(end); + + /// Riga per il registro: dice cosa succede e quando finisce. + public static string Spiegazione(int start, int end) => + $"puntata saltata: fascia oraria sospesa ({Normalize(start):00}:00-{Normalize(end):00}:00). " + + "L'asta resta attiva e riprende a puntare da sola alla fine della fascia."; + + private static int Normalize(int hour) + { + var h = hour % 24; + return h < 0 ? h + 24 : h; + } + } +} diff --git a/Mimante/Utilities/SettingsManager.cs b/Mimante/Utilities/SettingsManager.cs index 3c2df4e..6c446a8 100644 --- a/Mimante/Utilities/SettingsManager.cs +++ b/Mimante/Utilities/SettingsManager.cs @@ -561,6 +561,22 @@ namespace AutoBidder.Utilities /// public bool HardcodedAntiCollisionEnabled { get; set; } = false; + // ── Fascia oraria sospesa ──────────────────────────────────────── + // Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il + // doppio che fra le 10 e le 13. + + /// + /// Sospendere le puntate nella fascia indicata. Acceso di serie. + /// L'asta resta comunque Attiva e riprende da sola: vedi . + /// + public bool QuietHoursEnabled { get; set; } = true; + + /// Prima ora della fascia sospesa, inclusa. + public int QuietHoursStart { get; set; } = BiddingHours.DefaultStart; + + /// Prima ora in cui si torna a puntare. + public int QuietHoursEnd { get; set; } = BiddingHours.DefaultEnd; + /// /// Ritirarsi quando dall'altra parte c'è un'autopuntata armata. Acceso di serie. /// Vedi per il meccanismo e i numeri misurati.