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.