Semplifica timing puntata, logging e controllo convenienza

- Timing di puntata ora gestito solo da offset fisso configurabile, rimosse strategie di compensazione latenza/jitter/offset dinamico
- Aggiunto controllo convenienza: blocca puntate se il costo supera il "Compra Subito" oltre una soglia configurabile
- Logging granulare: nuove opzioni per log selettivo (puntate, strategie, valore, competizione, timing, errori, stato, profiling avversari)
- Persistenza stato browser aste (categoria, ricerca) tramite ApplicationStateService
- Fix conteggio puntate per bidder, rimosso rilevamento "Last Second Sniper", aggiunta strategia "Price Momentum"
- Refactoring e pulizia: rimozione codice obsoleto, migliorata documentazione e thread-safety
This commit is contained in:
2026-02-05 09:28:58 +01:00
parent 8befcb8abf
commit 0764b0b625
8 changed files with 594 additions and 446 deletions

View File

@@ -65,10 +65,40 @@ namespace AutoBidder.Models
[JsonPropertyName("BidsUsedOnThisAuction")]
public int? BidsUsedOnThisAuction { get; set; }
// Timestamp
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastClickAt { get; set; }
// ?? NUOVO: Sistema timing basato su deadline
/// <summary>
/// Timestamp UTC preciso della scadenza dell'asta.
/// Calcolato come: DateTime.UtcNow + Timer (quando riceviamo lo stato)
/// </summary>
[JsonIgnore]
public DateTime? DeadlineUtc { get; set; }
/// <summary>
/// Timestamp UTC dell'ultimo aggiornamento della deadline.
/// Usato per rilevare reset del timer.
/// </summary>
[JsonIgnore]
public DateTime? LastDeadlineUpdateUtc { get; set; }
/// <summary>
/// Timer raw dell'ultimo stato ricevuto (in secondi).
/// Usato per rilevare cambiamenti nel timer.
/// </summary>
[JsonIgnore]
public double LastRawTimer { get; set; }
/// <summary>
/// True se la puntata è già stata schedulata per questo ciclo.
/// Resettato quando il timer si resetta.
/// </summary>
[JsonIgnore]
public bool BidScheduled { get; set; }
// Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);

View File

@@ -1,4 +1,4 @@
@page "/browser"
@page "/browser"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using AutoBidder.Models
@using AutoBidder.Services
@@ -220,18 +220,18 @@
<h6 class="auction-name" title="@auction.Name">@auction.Name</h6>
<div class="auction-price">
<span class="current-price">@auction.CurrentPrice.ToString("0.00") €</span>
<span class="current-price">@auction.CurrentPrice.ToString("0.00") €</span>
@if (auction.BuyNowPrice > 0)
{
<span class="buynow-price text-muted">
<small>Compra: @auction.BuyNowPrice.ToString("0.00") €</small>
<small>Compra: @auction.BuyNowPrice.ToString("0.00") €</small>
</span>
}
</div>
<div class="auction-bidder">
<i class="bi bi-person-fill text-muted me-1"></i>
<span>@(string.IsNullOrEmpty(auction.LastBidder) ? "—" : auction.LastBidder)</span>
<span>@(string.IsNullOrEmpty(auction.LastBidder) ? "—" : auction.LastBidder)</span>
</div>
<div class="auction-timer @(auction.RemainingSeconds <= 3 ? "urgent" : "")">
@@ -295,7 +295,14 @@
private List<BidooCategoryInfo> categories = new();
private List<BidooBrowserAuction> auctions = new();
private List<BidooBrowserAuction> filteredAuctions = new();
private int selectedCategoryIndex = 0;
// 🔥 Usa stato persistente da AppState
private int selectedCategoryIndex
{
get => AppState.BrowserCategoryIndex;
set => AppState.BrowserCategoryIndex = value;
}
private int currentPage = 0;
private bool isLoading = false;
@@ -303,8 +310,12 @@ private bool isLoadingMore = false;
private bool canLoadMore = true;
private string? errorMessage = null;
// ? NUOVO: Ricerca
private string searchQuery = "";
// 🔥 Usa stato persistente per la ricerca
private string searchQuery
{
get => AppState.BrowserSearchQuery;
set => AppState.BrowserSearchQuery = value;
}
private System.Threading.Timer? stateUpdateTimer;
private CancellationTokenSource? cts;
@@ -314,9 +325,20 @@ private bool isUpdatingInBackground = false;
{
await LoadCategories();
// 🔥 Se c'è una categoria salvata, carica le aste
if (categories.Count > 0)
{
await LoadAuctions();
// Se selectedCategoryIndex è valido, carica quella categoria
if (selectedCategoryIndex >= 0 && selectedCategoryIndex < categories.Count)
{
await LoadAuctions();
}
else
{
// Altrimenti carica la prima categoria
selectedCategoryIndex = 0;
await LoadAuctions();
}
}
// Auto-update states every 500ms for real-time price updates
@@ -532,9 +554,30 @@ private bool isUpdatingInBackground = false;
{
if (browserAuction.IsMonitored) return;
// ?? Carica impostazioni di default
// 🔥 Carica impostazioni di default
var settings = AutoBidder.Utilities.SettingsManager.Load();
// 🔥 Determina stato iniziale da impostazioni
bool isActive = false;
bool isPaused = false;
switch (settings.DefaultNewAuctionState)
{
case "Active":
isActive = true;
isPaused = false;
break;
case "Paused":
isActive = true;
isPaused = true;
break;
case "Stopped":
default:
isActive = false;
isPaused = false;
break;
}
var auctionInfo = new AuctionInfo
{
AuctionId = browserAuction.AuctionId,
@@ -542,7 +585,7 @@ private bool isUpdatingInBackground = false;
OriginalUrl = browserAuction.Url,
BuyNowPrice = (double)browserAuction.BuyNowPrice,
// ?? FIX: Applica valori dalle impostazioni
// 🔥 Applica valori dalle impostazioni
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
@@ -550,8 +593,9 @@ private bool isUpdatingInBackground = false;
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
IsActive = true,
IsPaused = true, // Start paused
// 🔥 Usa stato da impostazioni invece di hardcoded
IsActive = isActive,
IsPaused = isPaused,
AddedAt = DateTime.UtcNow
};
@@ -565,7 +609,7 @@ private bool isUpdatingInBackground = false;
// Save to disk
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
// ?? FIX CRITICO: Avvia il monitor se non è già attivo
// ?? FIX CRITICO: Avvia il monitor se non è già attivo
if (!AppState.IsMonitoringActive)
{
AuctionMonitor.Start();

View File

@@ -181,80 +181,10 @@
</h2>
<div id="collapse-strategies" class="accordion-collapse collapse" aria-labelledby="heading-strategies" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<h6 class="fw-bold mb-3"><i class="bi bi-speedometer2"></i> Timing & Latenza</h6>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="adaptiveLatency" @bind="settings.AdaptiveLatencyEnabled" />
<label class="form-check-label" for="adaptiveLatency">
<strong>Compensazione latenza adattiva</strong>
<div class="form-text">Misura e compensa automaticamente la latenza di rete</div>
</label>
</div>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="jitter" @bind="settings.JitterEnabled" />
<label class="form-check-label" for="jitter">
<strong>Jitter casuale</strong>
<div class="form-text">Aggiunge variazione random per evitare sincronizzazione con altri bot</div>
</label>
</div>
</div>
<div class="col-12 col-md-4">
<label class="form-label"><i class="bi bi-shuffle"></i> Range jitter (±ms)</label>
<input type="number" class="form-control" @bind="settings.JitterRangeMs" />
</div>
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="dynamicOffset" @bind="settings.DynamicOffsetEnabled" />
<label class="form-check-label" for="dynamicOffset">
<strong>Offset dinamico</strong>
<div class="form-text">Adatta l'anticipo in base a heat, collisioni e volatilità</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Offset minimo (ms)</label>
<input type="number" class="form-control" @bind="settings.MinimumOffsetMs" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Offset massimo (ms)</label>
<input type="number" class="form-control" @bind="settings.MaximumOffsetMs" />
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-robot"></i> Strategia Anti-AutoBid Bidoo</h6>
<div class="alert alert-info mb-3">
<i class="bi bi-info-circle"></i>
<strong>Come funziona:</strong> Bidoo ha un sistema di auto-puntata integrato che si attiva a ~2 secondi.
Aspettando che il timer scenda sotto questa soglia, lasciamo che gli utenti con auto-puntata attiva
puntino prima di noi, <strong>risparmiando puntate</strong>.
</div>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="waitAutoBid" @bind="settings.WaitForAutoBidEnabled" />
<label class="form-check-label" for="waitAutoBid">
<strong>Attendi auto-puntate Bidoo</strong>
<div class="form-text">Aspetta che il timer scenda sotto la soglia prima di puntare (consigliato)</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Soglia attesa (secondi)</label>
<input type="number" step="0.1" class="form-control" @bind="settings.WaitForAutoBidThresholdSeconds" />
<div class="form-text">Punta solo quando timer &lt; questo valore (default: 1.8s)</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch mt-4">
<input type="checkbox" class="form-check-input" id="logAutoBid" @bind="settings.LogAutoBidWaitSkips" />
<label class="form-check-label" for="logAutoBid">
<strong>Log attese</strong>
<div class="form-text">Logga quando salta per aspettare (verbose)</div>
</label>
</div>
</div>
<div class="alert alert-info border-0 shadow-sm mb-4">
<i class="bi bi-info-circle me-2"></i>
<strong>Nota:</strong> Le strategie decidono <strong>SE</strong> puntare, non <strong>QUANDO</strong>.
Il timing è controllato solo dall'impostazione "Anticipo puntata" nelle Impostazioni Predefinite.
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-thermometer-half"></i> Rilevamento Competizione</h6>
@@ -287,6 +217,27 @@
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-cash-coin"></i> Controllo Convenienza</h6>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="valueCheck" @bind="settings.ValueCheckEnabled" />
<label class="form-check-label" for="valueCheck">
<strong>Blocca puntate non convenienti</strong>
<div class="form-text">Blocca se il costo totale supera il prezzo "Compra Subito" oltre la soglia</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Risparmio minimo richiesto (%)</label>
<input type="number" step="0.1" class="form-control" @bind="settings.MinSavingsPercentage" />
<div class="form-text">
Negativo = tolleranza perdita. Es: -5 = permetti fino al 5% di perdita<br />
Positivo = richiede risparmio. Es: 10 = richiedi almeno 10% di risparmio
</div>
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-arrow-left-right"></i> Soft Retreat</h6>
<div class="row g-3 mb-4">
<div class="col-12">
@@ -308,6 +259,7 @@
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-dice-5"></i> Puntata Probabilistica</h6>
<div class="row g-3 mb-4">
<div class="col-12">
@@ -421,25 +373,113 @@
</div>
</div>
<!-- LIMITI LOG -->
<!-- LOGGING GRANULARE -->
<div class="accordion-item">
<h2 class="accordion-header" id="heading-logs">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-logs" aria-expanded="false" aria-controls="collapse-logs">
<i class="bi bi-journal-text me-2"></i> Limiti Log
<i class="bi bi-journal-text me-2"></i> Logging e Limiti
</button>
</h2>
<div id="collapse-logs" class="accordion-collapse collapse" aria-labelledby="heading-logs" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<div class="row g-3">
<h6 class="fw-bold mb-3"><i class="bi bi-filter"></i> Cosa Loggare</h6>
<p class="text-muted small mb-3">Scegli quali informazioni visualizzare nei log delle aste.</p>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logBids" @bind="settings.LogBids" />
<label class="form-check-label" for="logBids">
<strong>[BID] Puntate piazzate</strong>
<div class="form-text">Log quando viene effettuata una puntata</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logStrategy" @bind="settings.LogStrategyDecisions" />
<label class="form-check-label" for="logStrategy">
<strong>[STRATEGY] Decisioni strategie</strong>
<div class="form-text">Log quando una strategia blocca la puntata</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logStatus" @bind="settings.LogAuctionStatus" />
<label class="form-check-label" for="logStatus">
<strong>[STATUS] Stato asta</strong>
<div class="form-text">Log quando l'asta termina, si resetta, ecc.</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logErrors" @bind="settings.LogErrors" />
<label class="form-check-label" for="logErrors">
<strong>[ERROR/WARN] Errori e warning</strong>
<div class="form-text">Log errori e avvisi importanti</div>
</label>
</div>
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-bug"></i> Debug Avanzato</h6>
<div class="alert alert-warning border-0 shadow-sm mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Attenzione:</strong> Queste opzioni generano molti log. Attiva solo per debug!
</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logTiming" @bind="settings.LogTiming" />
<label class="form-check-label" for="logTiming">
<strong>[TIMING] Timing puntate</strong>
<div class="form-text">Log dettagliato timing (molto verbose!)</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logValue" @bind="settings.LogValueCalculations" />
<label class="form-check-label" for="logValue">
<strong>[VALUE] Calcoli valore</strong>
<div class="form-text">Log calcoli convenienza prodotto</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logCompetition" @bind="settings.LogCompetition" />
<label class="form-check-label" for="logCompetition">
<strong>[COMPETITION] Competizione</strong>
<div class="form-text">Log rilevamento competizione e heat</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logOpponent" @bind="settings.LogOpponentProfiling" />
<label class="form-check-label" for="logOpponent">
<strong>[OPPONENT] Profiling avversari</strong>
<div class="form-text">Log analisi bidder aggressivi</div>
</label>
</div>
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-sliders"></i> Limiti</h6>
<div class="row g-3">
<div class="col-12 col-md-4">
<label class="form-label fw-bold"><i class="bi bi-list-ul"></i> Righe log globale</label>
<input type="number" class="form-control" @bind="settings.MaxGlobalLogLines" />
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<label class="form-label fw-bold"><i class="bi bi-list-check"></i> Righe log per asta</label>
<input type="number" class="form-control" @bind="settings.MaxLogLinesPerAuction" />
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<label class="form-label fw-bold"><i class="bi bi-clock-history"></i> Voci storia puntate</label>
<input type="number" class="form-control" @bind="settings.MaxBidHistoryEntries" />
</div>

View File

@@ -148,6 +148,47 @@ namespace AutoBidder.Services
}
}
// === STATO AUCTION BROWSER ===
private int _browserCategoryIndex = 0;
private string _browserSearchQuery = "";
public int BrowserCategoryIndex
{
get
{
lock (_lock)
{
return _browserCategoryIndex;
}
}
set
{
lock (_lock)
{
_browserCategoryIndex = value;
}
}
}
public string BrowserSearchQuery
{
get
{
lock (_lock)
{
return _browserSearchQuery;
}
}
set
{
lock (_lock)
{
_browserSearchQuery = value;
}
}
}
// === METODI GESTIONE ASTE ===
public void SetAuctions(List<AuctionInfo> auctions)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -98,7 +98,7 @@ namespace AutoBidder.Services
if (!_auctions.Any(a => a.AuctionId == auction.AuctionId))
{
_auctions.Add(auction);
// ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con defaults e stato
// ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con defaults e stato
// OnLog?.Invoke($"[+] Asta aggiunta: {auction.Name} (ID: {auction.AuctionId})");
}
}
@@ -111,20 +111,20 @@ namespace AutoBidder.Services
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
if (auction != null)
{
// ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla
// ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla
if (!auction.IsActive && auction.LastState != null)
{
OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso...");
try
{
// Determina se è stata vinta dall'utente
// Determina se è stata vinta dall'utente
var won = IsAuctionWonByUser(auction);
OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}");
// Emetti evento per salvare le statistiche
// Questo trigger sarà gestito in Program.cs con scraping HTML
// Questo trigger sarà gestito in Program.cs con scraping HTML
OnAuctionCompleted?.Invoke(auction, auction.LastState, won);
}
catch (Exception ex)
@@ -143,7 +143,7 @@ namespace AutoBidder.Services
}
/// <summary>
/// Determina se l'asta è stata vinta dall'utente corrente
/// Determina se l'asta è stata vinta dall'utente corrente
/// </summary>
private bool IsAuctionWonByUser(AuctionInfo auction)
{
@@ -154,7 +154,7 @@ namespace AutoBidder.Services
if (string.IsNullOrEmpty(username)) return false;
// Controlla se l'ultimo puntatore è l'utente
// Controlla se l'ultimo puntatore è l'utente
return auction.LastState.LastBidder?.Equals(username, StringComparison.OrdinalIgnoreCase) == true;
}
@@ -288,26 +288,41 @@ namespace AutoBidder.Services
continue;
}
// Delay adattivo OTTIMIZZATO basato su timer più basso
var lowestTimer = activeAuctions
.Select(a => GetLastTimer(a))
.Where(t => t > 0)
.DefaultIfEmpty(999)
.Min();
// ?? Delay adattivo ULTRA-OTTIMIZZATO
// Considera l'offset target più basso tra tutte le aste attive
var settings = Utilities.SettingsManager.Load();
int delayMs = lowestTimer switch
// ?? Polling VELOCE quando vicino alla scadenza
int minDelayMs = 500;
foreach (var a in activeAuctions)
{
< 0.5 => 10, // Ultra-critico: polling ogni 10ms
< 1 => 20, // Iper-veloce: polling ogni 20ms
< 2 => 50, // Ultra-veloce: polling ogni 50ms
< 3 => 100, // Molto veloce: polling ogni 100ms
< 5 => 150, // Veloce: polling ogni 150ms
< 10 => 300, // Medio: polling ogni 300ms
< 30 => 500, // Lento: polling ogni 500ms
_ => 1000 // Molto lento: polling ogni 1s
};
if (a.IsPaused || !a.LastDeadlineUpdateUtc.HasValue) continue;
await Task.Delay(delayMs, token);
// Calcola tempo stimato rimanente
var elapsed = (DateTime.UtcNow - a.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
double estimatedRemaining = a.LastRawTimer - elapsed;
int offsetMs = a.BidBeforeDeadlineMs > 0
? a.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
double msToTarget = estimatedRemaining - offsetMs;
// Polling più veloce quando vicino al target
int delay;
if (msToTarget < 100) delay = 5;
else if (msToTarget < 300) delay = 10;
else if (msToTarget < 500) delay = 20;
else if (msToTarget < 1000) delay = 50;
else if (msToTarget < 2000) delay = 100;
else if (msToTarget < 5000) delay = 200;
else delay = 400;
minDelayMs = Math.Min(minDelayMs, delay);
}
await Task.Delay(minDelayMs, token);
}
catch (OperationCanceledException)
{
@@ -355,7 +370,7 @@ namespace AutoBidder.Services
// ?? Aggiorna latenza con storico
auction.AddLatencyMeasurement(state.PollingLatencyMs);
// ?? Segna tracking dall'inizio se è la prima volta
// ?? Segna tracking dall'inizio se è la prima volta
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
{
auction.IsTrackedFromStart = true;
@@ -368,7 +383,7 @@ namespace AutoBidder.Services
MergeBidHistory(auction, state.RecentBidsHistory);
}
// 🔥 FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio)
// ?? FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio)
// Questo assicura che la puntata vincente sia sempre inclusa
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
{
@@ -391,7 +406,7 @@ namespace AutoBidder.Services
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var statePrice = (decimal)state.Price;
// Verifica se questa puntata non è già presente
// Verifica se questa puntata non è già presente
var alreadyExists = auction.RecentBids.Any(b =>
Math.Abs(b.Price - statePrice) < 0.001m &&
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
@@ -441,7 +456,7 @@ namespace AutoBidder.Services
if (state.Status == AuctionStatus.Running)
{
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
// Solo eventi importanti (bid, reset, errori) vengono loggati
}
else if (state.Status == AuctionStatus.Paused)
@@ -457,11 +472,15 @@ namespace AutoBidder.Services
OnAuctionUpdated?.Invoke(state);
UpdateAuctionHistory(auction, state);
// ?? NUOVO: Calcola e aggiorna valore prodotto se disponibili dati
if (auction.BuyNowPrice.HasValue || auction.ShippingCost.HasValue)
{
try
{
var settings = Utilities.SettingsManager.Load();
var productValue = Utilities.ProductValueCalculator.Calculate(
auction,
state.Price,
@@ -470,9 +489,9 @@ namespace AutoBidder.Services
auction.CalculatedValue = productValue;
// Log valore solo se cambia significativamente o ogni 10 polling
// Log valore solo se abilitato nelle impostazioni
bool shouldLogValue = false;
if (auction.PollingLatencyMs % 10 == 0) // Ogni ~10 poll
if (settings.LogValueCalculations && auction.PollingLatencyMs % 10 == 0) // Ogni ~10 poll
{
shouldLogValue = true;
}
@@ -490,13 +509,10 @@ namespace AutoBidder.Services
}
}
// NUOVA LOGICA: Punta solo se siamo vicini alla deadline E nessun altro ha appena puntato
if (state.Status == AuctionStatus.Running && !auction.IsPaused && !auction.IsAttackInProgress)
// ?? NUOVA LOGICA: Controlla PRIMA il timing, POI le strategie
if (state.Status == AuctionStatus.Running && !auction.IsPaused)
{
if (ShouldBid(auction, state))
{
await ExecuteBidStrategy(auction, state, token);
}
await TryPlaceBid(auction, state, token);
}
}
catch (Exception ex)
@@ -507,116 +523,112 @@ namespace AutoBidder.Services
}
/// <summary>
/// Strategia di puntata SEMPLIFICATA
/// Le strategie restituiscono solo un booleano (posso puntare?).
/// Il timing è sempre fisso: offset globale o override per asta.
/// Sistema di puntata SEMPLICE.
/// Punta esattamente a `offset` ms dalla scadenza.
/// Le strategie decidono SE puntare, non QUANDO.
/// </summary>
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
private async Task TryPlaceBid(AuctionInfo auction, AuctionState state, CancellationToken token)
{
var settings = SettingsManager.Load();
// Calcola il tempo rimanente in millisecondi
double timerMs = state.Timer * 1000;
// 🛑 CONTROLLO: Se sono già il vincitore, non fare nulla
if (state.IsMyBid)
{
return;
}
// 🎯 OFFSET FISSO: Usa override asta se impostato, altrimenti globale
int fixedOffsetMs = auction.BidBeforeDeadlineMs > 0
// Offset: millisecondi prima della scadenza
int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
// 🧠 VALUTAZIONE STRATEGIE (restituiscono solo bool)
var session = _apiClient.GetSession();
var currentUsername = session?.Username ?? "";
// Timer dall'API (in secondi, convertito in ms)
double timerMs = state.Timer * 1000;
// Verifica se le strategie permettono di puntare
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
// Skip se già vincitore o timer scaduto
if (state.IsMyBid || timerMs <= 0) return;
if (!decision.ShouldBid)
// ?? STIMA TEMPO RIMANENTE
// L'API dà timer in secondi interi (1000, 2000, ecc.)
// Quando cambia, salvo il timestamp. Poi stimo localmente.
bool timerChanged = Math.Abs(auction.LastRawTimer - timerMs) > 500;
if (timerChanged || !auction.LastDeadlineUpdateUtc.HasValue)
{
auction.LastRawTimer = timerMs;
auction.LastDeadlineUpdateUtc = DateTime.UtcNow;
auction.BidScheduled = false;
}
// Calcola tempo stimato rimanente
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
double estimatedRemaining = timerMs - elapsed;
// Log timing solo se abilitato
if (settings.LogTiming)
{
auction.AddLog($"[TIMING] API={timerMs:F0}ms, Elapsed={elapsed:F0}ms, Stima={estimatedRemaining:F0}ms");
}
// ?? È il momento di puntare?
// LOGICA MIGLIORATA: Punta quando siamo nell'ultimo secondo E stima <= offset
// Ma con un fallback: se timer=1000ms e stima < 300ms, punta comunque!
// Questo evita di perdere l'asta per polling troppo lento
bool shouldBidNow = false;
if (timerMs <= 1000 && timerMs > 0)
{
// Siamo nell'ultimo secondo
if (estimatedRemaining <= offsetMs)
{
// Stima sotto l'offset: PUNTA!
shouldBidNow = true;
}
else if (estimatedRemaining <= 300)
{
// Fallback: stima < 300ms ma sopra offset?
// Punta comunque per sicurezza (meglio puntare "presto" che perdere l'asta)
shouldBidNow = true;
}
}
else if (estimatedRemaining <= offsetMs && estimatedRemaining > -200)
{
// Logica originale per timer > 1 secondo
shouldBidNow = true;
}
if (!shouldBidNow) return;
// Protezione doppia puntata
if (auction.BidScheduled) return;
// Cooldown 1 secondo
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) return;
// ?? CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue)
if (!ShouldBid(auction, state))
{
auction.AddLog($"[STRATEGY] {decision.Reason}");
return;
}
// 🕐 TIMER-BASED SCHEDULING (offset fisso, niente calcoli dinamici)
if (timerMs > fixedOffsetMs)
// ? MOMENTO GIUSTO! Verifica strategie avanzate
var session = _apiClient.GetSession();
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, session?.Username ?? "");
if (!decision.ShouldBid)
{
// Timer ancora alto - Schedula puntata futura
double delayMs = timerMs - fixedOffsetMs;
// Non schedulare se già c'è un task attivo per questa asta
if (auction.IsAttackInProgress)
if (settings.LogStrategyDecisions)
{
return;
auction.AddLog($"[STRATEGY] {decision.Reason}");
}
auction.IsAttackInProgress = true;
auction.LastUsedOffsetMs = fixedOffsetMs;
auction.AddLog($"[TIMING] Timer={timerMs:F0}ms - Puntata tra {delayMs:F0}ms (offset fisso={fixedOffsetMs}ms)");
// Avvia task asincrono che attende e poi punta
_ = Task.Run(async () =>
{
try
{
// Attendi il momento esatto
await Task.Delay((int)delayMs, token);
// Verifica che l'asta sia ancora attiva e non in pausa
if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested)
{
auction.AddLog($"[TIMING] Task annullato (asta inattiva/pausa)");
return;
}
auction.AddLog($"[TIMING] Eseguo puntata!");
// Esegui la puntata
await ExecuteBid(auction, state, token);
}
catch (OperationCanceledException)
{
auction.AddLog($"[TIMING] Task cancellato");
}
catch (Exception ex)
{
auction.AddLog($"[TIMING ERROR] {ex.Message}");
}
finally
{
auction.IsAttackInProgress = false;
}
}, token);
return;
}
else if (timerMs > 0 && timerMs <= fixedOffsetMs)
// ?? PUNTA!
auction.BidScheduled = true;
if (settings.LogBids)
{
// Timer già nella finestra - Punta SUBITO senza delay
if (auction.IsAttackInProgress)
{
return;
}
auction.IsAttackInProgress = true;
auction.LastUsedOffsetMs = fixedOffsetMs;
try
{
auction.AddLog($"[TIMING] Timer in finestra ({timerMs:F0}ms <= {fixedOffsetMs}ms) - PUNTA SUBITO!");
// Esegui la puntata
await ExecuteBid(auction, state, token);
}
finally
{
auction.IsAttackInProgress = false;
}
auction.AddLog($"[BID] Puntata a ~{estimatedRemaining:F0}ms dalla scadenza");
}
// Se timer <= 0, asta già scaduta - Non fare nulla
await ExecuteBid(auction, state, token);
}
/// <summary>
@@ -639,7 +651,7 @@ namespace AutoBidder.Services
_bidStrategy.RecordTimerExpired(auction);
}
// 🔥 FIX: Aggiorna contatore puntate
// ?? FIX: Aggiorna contatore puntate
if (result.Success)
{
if (result.RemainingBids.HasValue)
@@ -681,7 +693,7 @@ namespace AutoBidder.Services
Timer = state.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : (result.Error ?? "Errore sconosciuto")
});
}
catch (Exception ex)
@@ -695,40 +707,33 @@ namespace AutoBidder.Services
{
var settings = Utilities.SettingsManager.Load();
// ?? CONTROLLO ANTI-AUTOBID BIDOO (PRIORITÀ MASSIMA)
// Bidoo ha un sistema di auto-puntata che si attiva a ~2 secondi.
// Aspettiamo che il timer scenda sotto la soglia per lasciare che
// gli altri utenti con auto-puntata attiva puntino prima di noi.
// Questo ci fa risparmiare puntate perché non puntiamo "troppo presto".
if (settings.WaitForAutoBidEnabled && state.Timer > settings.WaitForAutoBidThresholdSeconds)
if (settings.LogTiming)
{
// Timer ancora sopra la soglia - aspetta che le auto-puntate si attivino
if (settings.LogAutoBidWaitSkips)
{
auction.AddLog($"[AUTOBID] Timer {state.Timer:F2}s > soglia {settings.WaitForAutoBidThresholdSeconds}s - Aspetto auto-puntate Bidoo");
}
return false;
auction.AddLog($"[DEBUG] === INIZIO CONTROLLI PUNTATA ===");
}
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
if (auction.BuyNowPrice.HasValue &&
// ?? CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili)
if (settings.ValueCheckEnabled &&
auction.BuyNowPrice.HasValue &&
auction.BuyNowPrice.Value > 0 &&
auction.CalculatedValue != null &&
auction.CalculatedValue.Savings.HasValue &&
!auction.CalculatedValue.IsWorthIt)
{
// Permetti comunque di puntare se il risparmio è ancora positivo (anche se piccolo)
// Blocca solo se sta andando in perdita significativa (< -5%)
// Usa la percentuale configurabile dall'utente
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < -5)
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage)
{
auction.AddLog($"[VALUE] Puntata bloccata: perdita {auction.CalculatedValue.SavingsPercentage.Value:F1}% (troppo costoso)");
auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto");
return false;
}
}
if (settings.LogTiming && settings.ValueCheckEnabled)
{
auction.AddLog($"[DEBUG] ? Controllo convenienza OK");
}
// ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate"
// Se negli ultimi 10 secondi ci sono state 3+ puntate di utenti diversi, evita
var recentBidsThreshold = 10; // secondi
@@ -746,9 +751,14 @@ namespace AutoBidder.Services
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
}
if (activeBidders >= maxActiveBidders)
{
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
var session = _apiClient.GetSession();
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
@@ -762,6 +772,11 @@ namespace AutoBidder.Services
}
catch { /* Ignora errori nel controllo competizione */ }
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] ? Controllo competizione OK");
}
// ?? CONTROLLO 1: Limite minimo puntate residue
if (settings.MinimumRemainingBids > 0)
{
@@ -771,27 +786,44 @@ namespace AutoBidder.Services
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
return false;
}
if (settings.LogTiming && session != null)
{
auction.AddLog($"[DEBUG] ? Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
}
}
// ? CONTROLLO 2: Non puntare se sono già il vincitore corrente
// ? CONTROLLO 2: Non puntare se sono già il vincitore corrente
if (state.IsMyBid)
{
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] Sono già vincitore");
}
return false;
}
// ?? CONTROLLO 3: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
return false;
}
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
return false;
}
if (settings.LogTiming)
{
if (auction.MinPrice > 0 || auction.MaxPrice > 0)
{
auction.AddLog($"[DEBUG] ? Range prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {auction.MaxPrice:F2}])");
}
}
// ?? CONTROLLO 4: MinResets/MaxResets
if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets)
{
@@ -805,16 +837,34 @@ namespace AutoBidder.Services
return false;
}
if (settings.LogTiming)
{
if (auction.MinResets > 0 || auction.MaxResets > 0)
{
auction.AddLog($"[DEBUG] ? Range reset OK ({auction.ResetCount} in [{auction.MinResets}, {auction.MaxResets}])");
}
}
// ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate)
if (auction.LastClickAt.HasValue)
{
var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value;
if (timeSinceLastClick.TotalMilliseconds < 800)
{
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] Cooldown attivo ({timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms)");
}
return false;
}
}
if (settings.LogTiming)
{
auction.AddLog($"[DEBUG] === TUTTI I CONTROLLI SUPERATI ===");
}
return true;
}
@@ -867,20 +917,8 @@ namespace AutoBidder.Services
Notes = $"Puntata: EUR{state.Price:F2}"
});
// Aggiorna statistiche bidder
if (!string.IsNullOrEmpty(state.LastBidder))
{
if (!auction.BidderStats.ContainsKey(state.LastBidder))
{
auction.BidderStats[state.LastBidder] = new BidderInfo
{
Username = state.LastBidder
};
}
auction.BidderStats[state.LastBidder].BidCount++;
auction.BidderStats[state.LastBidder].LastBidTime = DateTime.UtcNow;
}
// ? RIMOSSO: Non incrementare qui - è già gestito da UpdateBidderStatsFromRecentBids
// L'incremento doppio causava conteggi gonfiati
OnResetCountChanged?.Invoke(auction.AuctionId);
}
@@ -894,8 +932,8 @@ namespace AutoBidder.Services
/// <summary>
/// Unisce la storia puntate ricevuta dall'API con quella esistente,
/// mantenendo le puntate più vecchie e aggiungendo solo le nuove.
/// Le puntate sono ordinate con le più RECENTI in CIMA.
/// mantenendo le puntate più vecchie e aggiungendo solo le nuove.
/// Le puntate sono ordinate con le più RECENTI in CIMA.
/// </summary>
private void MergeBidHistory(AuctionInfo auction, List<BidHistoryEntry> newBids)
{
@@ -908,7 +946,7 @@ namespace AutoBidder.Services
// ?? FIX: Usa lock per thread-safety
lock (auction.RecentBids)
{
// Se la lista esistente è vuota, semplicemente copia le nuove
// Se la lista esistente è vuota, semplicemente copia le nuove
if (auction.RecentBids.Count == 0)
{
auction.RecentBids = newBids.ToList();
@@ -953,7 +991,7 @@ namespace AutoBidder.Services
.ThenByDescending(b => b.Price)
.ToList();
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
{
auction.RecentBids = auction.RecentBids
@@ -975,7 +1013,7 @@ namespace AutoBidder.Services
/// <summary>
/// Assicura che la puntata corrente (quella vincente) sia sempre presente nello storico.
/// Questo risolve il problema dell'API che a volte non include l'ultima puntata.
/// IMPORTANTE: Aggiunge solo se è una NUOVA puntata (prezzo/utente diverso dall'ultima registrata).
/// IMPORTANTE: Aggiunge solo se è una NUOVA puntata (prezzo/utente diverso dall'ultima registrata).
/// </summary>
private void EnsureCurrentBidInHistory(AuctionInfo auction, AuctionState state)
{
@@ -984,22 +1022,18 @@ namespace AutoBidder.Services
var statePrice = (decimal)state.Price;
var currentBidder = state.LastBidder;
// 🔥 VERIFICA: Controlla se è la stessa puntata che abbiamo già in cima
// Evitiamo di aggiungere continuamente la stessa puntata ad ogni polling
if (auction.RecentBids.Count > 0)
{
var topBid = auction.RecentBids[0]; // Prima = più recente
// ?? VERIFICA: Controlla se questa puntata è già presente nella lista
// Evitiamo duplicati controllando prezzo + utente in TUTTA la lista
var alreadyExists = auction.RecentBids.Any(b =>
Math.Abs(b.Price - statePrice) < 0.001m &&
b.Username.Equals(currentBidder, StringComparison.OrdinalIgnoreCase));
// Se la puntata in cima è identica (stesso prezzo + stesso utente), salta
if (Math.Abs(topBid.Price - statePrice) < 0.001m &&
topBid.Username.Equals(currentBidder, StringComparison.OrdinalIgnoreCase))
{
return; // Già presente in cima, non serve aggiungere
}
if (alreadyExists)
{
return; // Già presente, non serve aggiungere
}
// 🔥 NUOVA PUNTATA: Aggiungi solo se diversa dall'ultima
// Questo significa che c'è stata una nuova puntata che l'API non ha ancora segnalato
// ?? NUOVA PUNTATA: Aggiungi solo se non esiste già
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
auction.RecentBids.Insert(0, new BidHistoryEntry
@@ -1010,52 +1044,45 @@ namespace AutoBidder.Services
BidType = "Auto"
});
// Aggiorna anche le statistiche bidder
if (!auction.BidderStats.ContainsKey(currentBidder))
{
auction.BidderStats[currentBidder] = new BidderInfo
{
Username = currentBidder,
BidCount = 1,
RecentBidCount = 1,
LastBidTime = DateTime.UtcNow
};
}
else
{
var bidder = auction.BidderStats[currentBidder];
bidder.BidCount++;
bidder.RecentBidCount++;
bidder.LastBidTime = DateTime.UtcNow;
}
// ? RIMOSSO: Non incrementare BidderStats qui
// È gestito SOLO da UpdateBidderStatsFromRecentBids per evitare duplicazioni
// La puntata è stata aggiunta a RecentBids, sarà contata al prossimo aggiornamento
}
catch { /* Silenzioso */ }
}
/// <summary>
/// Aggiorna le statistiche dei bidder in modo CUMULATIVO.
/// BidCount è il totale dall'inizio del monitoraggio.
/// RecentBidCount è il conteggio nella finestra corrente di RecentBids.
/// I bidder NON vengono mai rimossi, il contatore è infinito.
/// Aggiorna le statistiche dei bidder basandosi SOLO su RecentBids.
/// QUESTO È L'UNICO POSTO che aggiorna i conteggi.
/// IMPORTANTE: Il conteggio è basato SOLO sulle puntate in RecentBids, non cumulativo.
/// </summary>
private void UpdateBidderStatsFromRecentBids(AuctionInfo auction)
{
try
{
// Raggruppa puntate per username nella finestra corrente
// ?? FIX: Conta direttamente le puntate in RecentBids per ogni utente
// Non usare delta, conta sempre da zero basandosi sulla lista attuale
var bidsByUser = auction.RecentBids
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => new
{
RecentCount = g.Count(),
Count = g.Count(),
LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime
},
StringComparer.OrdinalIgnoreCase
);
// Aggiorna o crea BidderInfo per ogni utente
// Resetta tutti i conteggi e ricalcola da zero
// Questo evita accumuli errati dovuti a delta sbagliati
foreach (var existing in auction.BidderStats.Values)
{
existing.BidCount = 0;
existing.RecentBidCount = 0;
}
// Aggiorna con i conteggi reali da RecentBids
foreach (var kvp in bidsByUser)
{
var username = kvp.Key;
@@ -1063,37 +1090,22 @@ namespace AutoBidder.Services
if (!auction.BidderStats.ContainsKey(username))
{
// Nuovo bidder - inizializza con i conteggi attuali
auction.BidderStats[username] = new BidderInfo
{
Username = username,
BidCount = stats.RecentCount, // Primo conteggio
RecentBidCount = stats.RecentCount,
BidCount = stats.Count,
RecentBidCount = stats.Count,
LastBidTime = stats.LastBidTime
};
}
else
{
var existing = auction.BidderStats[username];
// Calcola delta: quante nuove puntate rispetto all'ultimo check
int previousRecent = existing.RecentBidCount;
int currentRecent = stats.RecentCount;
// Se il conteggio è aumentato, aggiungi la differenza al totale cumulativo
if (currentRecent > previousRecent)
{
existing.BidCount += (currentRecent - previousRecent);
}
// Aggiorna conteggio finestra corrente e ultimo timestamp
existing.RecentBidCount = currentRecent;
existing.BidCount = stats.Count;
existing.RecentBidCount = stats.Count;
existing.LastBidTime = stats.LastBidTime;
}
}
// NON rimuovere i bidder che non sono più in RecentBids!
// Il conteggio totale è cumulativo e persistente.
}
catch (Exception ex)
{

View File

@@ -17,84 +17,6 @@ namespace AutoBidder.Services
private int _sessionTotalBids = 0;
private DateTime _sessionStartedAt = DateTime.UtcNow;
/// <summary>
/// Calcola l'offset ottimale per una puntata considerando tutti i fattori
/// </summary>
public BidTimingResult CalculateOptimalTiming(AuctionInfo auction, AppSettings settings)
{
var result = new BidTimingResult
{
BaseOffsetMs = auction.BidBeforeDeadlineMs,
FinalOffsetMs = auction.BidBeforeDeadlineMs,
ShouldBid = true
};
// 1. Adaptive Latency Compensation
if (settings.AdaptiveLatencyEnabled)
{
result.LatencyCompensationMs = (int)auction.AverageLatencyMs;
result.FinalOffsetMs += result.LatencyCompensationMs;
}
// 2. Dynamic Offset (basato su heat, storico, volatilità)
if (settings.DynamicOffsetEnabled)
{
var dynamicAdjustment = CalculateDynamicOffset(auction, settings);
result.DynamicAdjustmentMs = dynamicAdjustment;
result.FinalOffsetMs += dynamicAdjustment;
}
// 3. Jitter (randomizzazione)
if (settings.JitterEnabled || (auction.JitterEnabledOverride ?? settings.JitterEnabled))
{
result.JitterMs = _random.Next(-settings.JitterRangeMs, settings.JitterRangeMs + 1);
result.FinalOffsetMs += result.JitterMs;
}
// 4. Clamp ai limiti
result.FinalOffsetMs = Math.Clamp(result.FinalOffsetMs, settings.MinimumOffsetMs, settings.MaximumOffsetMs);
// Salva offset calcolato nell'asta
auction.DynamicOffsetMs = result.FinalOffsetMs;
return result;
}
/// <summary>
/// Calcola offset dinamico basato su heat, storico e volatilità
/// </summary>
private int CalculateDynamicOffset(AuctionInfo auction, AppSettings settings)
{
int adjustment = 0;
// Più l'asta è "calda", più anticipo serve
if (auction.HeatMetric > 50)
{
adjustment += (auction.HeatMetric - 50) / 2; // +0-25ms per heat 50-100
}
// Se ci sono state collisioni recenti, anticipa di più
if (auction.ConsecutiveCollisions > 0)
{
adjustment += auction.ConsecutiveCollisions * 10; // +10ms per ogni collisione
}
// Se la latenza è volatile (alta deviazione), aggiungi margine
if (auction.LatencyHistory.Count >= 5)
{
var avg = auction.LatencyHistory.Average();
var variance = auction.LatencyHistory.Sum(x => Math.Pow(x - avg, 2)) / auction.LatencyHistory.Count;
var stdDev = Math.Sqrt(variance);
if (stdDev > 20) // Alta variabilità
{
adjustment += (int)(stdDev / 2);
}
}
return adjustment;
}
/// <summary>
/// Aggiorna heat metric per un'asta
/// </summary>
@@ -360,9 +282,43 @@ namespace AutoBidder.Services
}
}
// ? RIMOSSO: DetectLastSecondSniper - causava falsi positivi
// In un duello, TUTTI i bidder hanno pattern regolari (ogni reset del timer)
// Questa strategia bloccava puntate legittime e faceva perdere aste
// ?? 7. STRATEGIA: Price Momentum (con soglia più alta)
// Se il prezzo sta salendo TROPPO velocemente, pausa
var priceVelocity = CalculatePriceVelocity(auction);
if (priceVelocity > 0.10) // +10 centesimi/secondo = MOLTO veloce
{
decision.ShouldBid = false;
decision.Reason = $"Prezzo sale troppo veloce ({priceVelocity:F3}€/s)";
return decision;
}
return decision;
}
/// <summary>
/// Calcola la velocità di crescita del prezzo (€/secondo)
/// </summary>
private double CalculatePriceVelocity(AuctionInfo auction)
{
if (auction.RecentBids.Count < 5) return 0;
var recentBids = auction.RecentBids.Take(10).ToList();
if (recentBids.Count < 2) return 0;
var first = recentBids.Last();
var last = recentBids.First();
var timeDiffSeconds = last.Timestamp - first.Timestamp;
if (timeDiffSeconds <= 0) return 0;
var priceDiff = last.Price - first.Price;
return (double)priceDiff / timeDiffSeconds;
}
/// <summary>
/// Rileva pattern bot analizzando i delta timing degli ultimi bid
/// </summary>

View File

@@ -41,7 +41,7 @@ namespace AutoBidder.Utilities
}
// Calcola risparmio rispetto al prezzo "Compra Subito"
if (auctionInfo.BuyNowPrice.HasValue)
if (auctionInfo.BuyNowPrice.HasValue && auctionInfo.BuyNowPrice.Value > 0)
{
var buyNowTotal = auctionInfo.BuyNowPrice.Value;
if (auctionInfo.ShippingCost.HasValue)
@@ -50,12 +50,24 @@ namespace AutoBidder.Utilities
}
value.Savings = buyNowTotal - value.TotalCostIfWin;
value.SavingsPercentage = (value.Savings.Value / buyNowTotal) * 100.0;
// ?? FIX: Evita divisione per zero
if (buyNowTotal > 0)
{
value.SavingsPercentage = (value.Savings.Value / buyNowTotal) * 100.0;
}
else
{
// Se il buyNowTotal è 0, imposta un valore fittizio negativo per indicare perdita
value.SavingsPercentage = -100.0;
}
value.IsWorthIt = value.Savings.Value > 0;
}
else
{
// Senza prezzo "Compra Subito", consideriamo sempre conveniente
// Senza prezzo "Compra Subito" valido, consideriamo sempre conveniente
// Questo permette di puntare su aste senza dati di riferimento
value.IsWorthIt = true;
}

View File

@@ -7,7 +7,7 @@ namespace AutoBidder.Utilities
public class AppSettings
{
// NUOVE IMPOSTAZIONI PREDEFINITE PER LE ASTE
public int DefaultBidBeforeDeadlineMs { get; set; } = 200;
public int DefaultBidBeforeDeadlineMs { get; set; } = 800;
public bool DefaultCheckAuctionOpenBeforeBid { get; set; } = false;
public double DefaultMinPrice { get; set; } = 0;
public double DefaultMaxPrice { get; set; } = 0;
@@ -104,71 +104,64 @@ namespace AutoBidder.Utilities
// STRATEGIE AVANZATE DI PUNTATA
// ???????????????????????????????????????????????????????????????
// ❌ RIMOSSO: Jitter, Offset Dinamico, Latenza Adattiva
// Il timing è gestito SOLO da DefaultBidBeforeDeadlineMs
// Le strategie decidono SE puntare, non QUANDO
// 🎯 LOGGING GRANULARE
// ???????????????????????????????????????????????????????????????
/// <summary>
/// Abilita compensazione adattiva della latenza.
/// Misura latenza reale per ogni asta e adatta l'anticipo automaticamente.
/// Log quando viene piazzata una puntata [BID]
/// Default: true
/// </summary>
public bool AdaptiveLatencyEnabled { get; set; } = true;
public bool LogBids { get; set; } = true;
/// <summary>
/// Abilita jitter casuale sull'offset per evitare sincronizzazione con altri bot.
/// Aggiunge ±JitterRangeMs al timing di puntata.
/// Log quando una strategia blocca la puntata [STRATEGY]
/// Default: true
/// </summary>
public bool JitterEnabled { get; set; } = true;
public bool LogStrategyDecisions { get; set; } = true;
/// <summary>
/// Range massimo del jitter casuale in millisecondi (±X ms).
/// Default: 50 (range -50ms a +50ms)
/// Log calcoli valore prodotto [VALUE]
/// Default: false (attiva per debug)
/// </summary>
public int JitterRangeMs { get; set; } = 50;
public bool LogValueCalculations { get; set; } = false;
/// <summary>
/// Abilita offset dinamico per asta basato su ping, storico e volatilità.
/// Log rilevamento competizione e heat [COMPETITION]
/// Default: false
/// </summary>
public bool LogCompetition { get; set; } = false;
/// <summary>
/// Log timing e polling (molto verbose!) [TIMING]
/// Default: false (attiva solo per debug timing)
/// </summary>
public bool LogTiming { get; set; } = false;
/// <summary>
/// Log errori e warning [ERROR/WARN]
/// Default: true
/// </summary>
public bool DynamicOffsetEnabled { get; set; } = true;
public bool LogErrors { get; set; } = true;
/// <summary>
/// Offset minimo garantito in ms (non scende mai sotto questo valore).
/// Default: 80
/// </summary>
public int MinimumOffsetMs { get; set; } = 80;
/// <summary>
/// Offset massimo in ms (non supera mai questo valore).
/// Default: 500
/// </summary>
public int MaximumOffsetMs { get; set; } = 500;
// ?? STRATEGIA ANTI-AUTOBID BIDOO
// Bidoo ha un sistema di auto-puntata integrato che si attiva a ~2 secondi.
// Aspettando che questa soglia venga superata, lasciamo che gli altri
// utenti con auto-puntata attiva puntino prima di noi, risparmiando puntate.
/// <summary>
/// Abilita la strategia di attesa per le auto-puntate di Bidoo.
/// Se true, aspetta che il timer scenda sotto la soglia prima di puntare.
/// Log stato asta (terminata, reset, ecc.) [STATUS]
/// Default: true
/// </summary>
public bool WaitForAutoBidEnabled { get; set; } = true;
public bool LogAuctionStatus { get; set; } = true;
/// <summary>
/// Soglia in secondi sotto la quale si può puntare.
/// Bidoo attiva le auto-puntate a ~2 secondi, quindi aspettiamo che passino.
/// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate)
/// Log profiling avversari [OPPONENT]
/// Default: false
/// </summary>
public double WaitForAutoBidThresholdSeconds { get; set; } = 1.8;
/// <summary>
/// Se true, logga quando salta una puntata per aspettare le auto-puntate.
/// Default: false (per evitare spam nel log)
/// </summary>
public bool LogAutoBidWaitSkips { get; set; } = false;
public bool LogOpponentProfiling { get; set; } = false;
// 🎯 STRATEGIE SEMPLIFICATE
/// <summary>
/// Entry Point: Usato SOLO per calcolare i limiti consigliati (70% del MaxPrice storico).
/// NON blocca le puntate! I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
@@ -190,6 +183,26 @@ namespace AutoBidder.Utilities
/// </summary>
public bool UserExhaustionEnabled { get; set; } = true;
// 🎯 CONTROLLO CONVENIENZA PRODOTTO
/// <summary>
/// Abilita il controllo di convenienza basato sul valore del prodotto.
/// Se attivo, blocca le puntate quando il costo totale supera il prezzo "Compra Subito"
/// di una percentuale superiore a MinSavingsPercentage.
/// Default: true
/// </summary>
public bool ValueCheckEnabled { get; set; } = true;
/// <summary>
/// Percentuale minima di risparmio richiesta per continuare a puntare.
/// Valori negativi = tolleranza alla perdita.
/// Es: -5 = permetti fino al 5% di perdita rispetto al "Compra Subito"
/// 0 = blocca se costa uguale o più del "Compra Subito"
/// 10 = richiedi almeno 10% di risparmio
/// Default: -5 (permetti fino al 5% di perdita)
/// </summary>
public double MinSavingsPercentage { get; set; } = -5.0;
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥