Sospendi le puntate nella fascia oraria in cui costano il doppio

Su 6060 aste concluse con prezzo e valore noti, la stessa asta non costa uguale a
tutte le ore. Prezzo di chiusura mediano in percentuale del valore del prodotto:
4,6% alle 12 e 5,3% fra le 10 e le 13, contro 8,6% a mezzanotte e 9,7% alle 9. Le
puntate necessarie 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 e' che di notte Bidoo sospende le puntate: restano poche aste in corso e
la concorrenza si concentra li'. Le ore di confine, mezzanotte e le nove, sono le
peggiori, perche' c'e' ancora qualcosa da giocare e ci sono tutti addosso.

La fascia predefinita e' 00:00-10:00. Sospende le puntate, NON il monitoraggio:
l'asta resta in stato Attiva, continua a essere seguita e riprende a puntare da
sola alla fine della fascia. E' voluto: le aste lunghe attraversano la fascia, e
fermarle davvero significherebbe perderle invece che risparmiare. Ogni puntata
saltata scrive nel registro il motivo e l'orario di ripresa.

Tre livelli di override, dal piu' specifico al piu' generale: la singola asta
scavalca il prodotto, il prodotto scavalca le impostazioni generali. Un null non
vuol dire "no" ma "non ho un'opinione", ed e' la differenza che permette di
spegnere il filtro su una sola asta senza toccare il resto.

Una fascia a cavallo di mezzanotte (22-6) funziona; inizio uguale a fine vuol dire
filtro spento, che e' l'unica lettura sensata visto che l'alternativa sarebbe non
puntare mai.

Backtest della regola sul duello, completato su tutte le soglie. Sui dossier veri
in cui il bot ha puntato: 13 aste, 366 puntate, zero vittorie. Con soglia 5 le
puntate sarebbero state 49, risparmiate 317 (86,6%), nessuna vittoria persa. Il
risultato tiene da soglia 2 a soglia 10, che e' quanto ci si puo' aspettare da un
campione in cui non si era vinto niente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 15:50:06 +02:00
co-authored by Claude Opus 5
parent 5bac29b0af
commit 8b9691d692
6 changed files with 239 additions and 0 deletions
+7
View File
@@ -704,6 +704,13 @@ namespace AutoBidder.Models
/// </summary>
public bool? SoftRetreatEnabledOverride { get; set; }
/// <summary>
/// Override per questa asta della sospensione a fascia oraria.
/// <c>null</c> = come dice il prodotto, e se il prodotto tace come dicono le
/// impostazioni globali. Vedi <see cref="Utilities.BiddingHours"/>.
/// </summary>
public bool? QuietHoursEnabledOverride { get; set; }
/// <summary>
/// Override: limite puntate per questa asta
/// </summary>
+7
View File
@@ -32,6 +32,13 @@ namespace AutoBidder.Models
/// <summary>Prezzo minimo sotto il quale non puntare. <c>null</c> = predefinito.</summary>
public double? MinPrice { get; set; }
/// <summary>
/// Sospendere le puntate nella fascia oraria. <c>null</c> = come le impostazioni
/// generali. Un'asta può a sua volta scavalcare questo valore.
/// Vedi <see cref="Utilities.BiddingHours"/>.
/// </summary>
public bool? QuietHoursEnabled { get; set; }
/// <summary>Prezzo oltre il quale lasciar perdere l'asta. <c>null</c> = predefinito.</summary>
public double? MaxPrice { get; set; }
+40
View File
@@ -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
/// <summary>
/// Registra una puntata effettuata (per tracking)
/// </summary>
/// <summary>
/// La sospensione a fascia oraria vale per quest'asta?
///
/// <para>Tre livelli, dal più specifico al più generale: l'asta scavalca il
/// prodotto, il prodotto scavalca le impostazioni generali. Un <c>null</c> non
/// significa "no", significa "non ho un'opinione": è la differenza che permette di
/// spegnere il filtro su una singola asta senza toccare il resto.</para>
/// </summary>
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++;
+92
View File
@@ -0,0 +1,92 @@
using System;
using AutoBidder.Utilities;
using Xunit;
namespace AutoBidder.Tests;
/// <summary>
/// Fascia oraria sospesa.
///
/// <para>La proprieta' da difendere e' che la fascia <b>non fermi l'asta</b>: 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.</para>
///
/// <para>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.</para>
/// </summary>
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));
}
}
+77
View File
@@ -0,0 +1,77 @@
using System;
namespace AutoBidder.Utilities
{
/// <summary>
/// Fascia oraria in cui non si punta.
///
/// <para><b>Perché.</b> 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 <b>8,6% a
/// mezzanotte</b> e <b>9,7% alle 9</b>. 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.</para>
///
/// <para>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.</para>
///
/// <para><b>Cosa fa.</b> 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.</para>
///
/// <para>Classe pura, l'ora arriva da fuori: così i test non dipendono da quando
/// vengono eseguiti.</para>
/// </summary>
public static class BiddingHours
{
/// <summary>Prima ora della fascia sospesa, inclusa. Predefinito: mezzanotte.</summary>
public const int DefaultStart = 0;
/// <summary>Prima ora in cui si torna a puntare. Predefinito: le 10.</summary>
public const int DefaultEnd = 10;
/// <summary>
/// L'ora indicata cade nella fascia sospesa?
///
/// <para>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.</para>
/// </summary>
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
}
/// <summary>Comodità: la fascia applicata a un istante.</summary>
public static bool IsQuiet(DateTime when, int start, int end) =>
IsQuiet(when.Hour, start, end);
/// <summary>
/// Quando riprende, per poterlo scrivere nel registro. Restituisce l'ora di ripresa
/// (0-23); chi legge sa già che è la prossima volta che scocca.
/// </summary>
public static int ResumesAt(int end) => Normalize(end);
/// <summary>Riga per il registro: dice cosa succede e quando finisce.</summary>
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;
}
}
}
+16
View File
@@ -561,6 +561,22 @@ namespace AutoBidder.Utilities
/// </summary>
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.
/// <summary>
/// Sospendere le puntate nella fascia indicata. Acceso di serie.
/// L'asta resta comunque Attiva e riprende da sola: vedi <see cref="BiddingHours"/>.
/// </summary>
public bool QuietHoursEnabled { get; set; } = true;
/// <summary>Prima ora della fascia sospesa, inclusa.</summary>
public int QuietHoursStart { get; set; } = BiddingHours.DefaultStart;
/// <summary>Prima ora in cui si torna a puntare.</summary>
public int QuietHoursEnd { get; set; } = BiddingHours.DefaultEnd;
/// <summary>
/// Ritirarsi quando dall'altra parte c'è un'autopuntata armata. Acceso di serie.
/// Vedi <see cref="AutoBidDuel"/> per il meccanismo e i numeri misurati.