- Ordinamento colonne griglia aste e indicatori visivi - Nuovo pulsante per rimozione rapida aste terminate - Log aste con deduplicazione e contatore - Statistiche puntatori cumulative e più affidabili - Cronologia puntate senza duplicati consecutivi - Strategie di puntata semplificate: entry point, anti-bot, user exhaustion - UI più compatta, hover moderni, evidenziazione puntate utente - Correzioni internazionalizzazione e pulizia codice
596 lines
24 KiB
C#
596 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using AutoBidder.Models;
|
|
using AutoBidder.Utilities;
|
|
|
|
namespace AutoBidder.Services
|
|
{
|
|
/// <summary>
|
|
/// Servizio per strategie avanzate di puntata.
|
|
/// Implementa: adaptive latency, jitter, dynamic offset, heat metric,
|
|
/// competition detection, soft retreat, probabilistic bidding, opponent profiling.
|
|
/// </summary>
|
|
public class BidStrategyService
|
|
{
|
|
private readonly Random _random = new();
|
|
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>
|
|
public void UpdateHeatMetric(AuctionInfo auction, AppSettings settings, string currentUsername = "")
|
|
{
|
|
if (!settings.CompetitionDetectionEnabled) return;
|
|
|
|
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
var windowStart = now - settings.CompetitionWindowSeconds;
|
|
|
|
// Conta bidder unici nella finestra temporale (escludo me stesso)
|
|
var recentBids = auction.RecentBids
|
|
.Where(b => b.Timestamp >= windowStart)
|
|
.Where(b => !b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
auction.ActiveBiddersCount = recentBids
|
|
.Select(b => b.Username)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.Count();
|
|
|
|
// Conta collisioni (puntate nello stesso secondo)
|
|
var bidsBySecond = recentBids
|
|
.GroupBy(b => b.Timestamp)
|
|
.Where(g => g.Count() > 1)
|
|
.Count();
|
|
|
|
auction.CollisionCount = bidsBySecond;
|
|
|
|
// Calcola heat metric (0-100)
|
|
// Fattori: bidder attivi (40%), frequenza puntate (30%), collisioni (30%)
|
|
|
|
int bidderScore = Math.Min(auction.ActiveBiddersCount * 15, 40); // Max 40 punti
|
|
int frequencyScore = Math.Min(recentBids.Count * 3, 30); // Max 30 punti
|
|
int collisionScore = Math.Min(auction.CollisionCount * 10, 30); // Max 30 punti
|
|
|
|
auction.HeatMetric = bidderScore + frequencyScore + collisionScore;
|
|
|
|
// Identifica bidder aggressivi e situazioni di duello
|
|
if (settings.OpponentProfilingEnabled)
|
|
{
|
|
UpdateAggressiveBidders(auction, settings, currentUsername);
|
|
DetectDuelSituation(auction, settings, currentUsername);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifica e tracca bidder aggressivi (basato su ultime N puntate, esclude utente corrente)
|
|
/// </summary>
|
|
private void UpdateAggressiveBidders(AuctionInfo auction, AppSettings settings, string currentUsername)
|
|
{
|
|
// ?? FIX: Usa finestra scorrevole di ultime N puntate
|
|
var windowSize = settings.AggressiveBidderWindowSize > 0 ? settings.AggressiveBidderWindowSize : 30;
|
|
var recentWindow = auction.RecentBids
|
|
.Take(windowSize)
|
|
.ToList();
|
|
|
|
var bidCounts = recentWindow
|
|
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
|
.Select(g => new { Username = g.Key, Count = g.Count(), Percentage = (double)g.Count() / recentWindow.Count * 100 })
|
|
.ToList();
|
|
|
|
auction.AggressiveBidders.Clear();
|
|
|
|
foreach (var bidder in bidCounts)
|
|
{
|
|
// ?? FIX: NON aggiungere l'utente corrente come aggressivo!
|
|
if (bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
// ?? FIX: Soglia più permissiva - usa percentuale invece di conteggio assoluto
|
|
// Un bidder è "aggressivo" se ha più del 40% delle puntate nella finestra (configurabile)
|
|
var percentageThreshold = settings.AggressiveBidderPercentageThreshold > 0 ? settings.AggressiveBidderPercentageThreshold : 40.0;
|
|
|
|
if (bidder.Percentage >= percentageThreshold || bidder.Count >= settings.AggressiveBidderThreshold)
|
|
{
|
|
auction.AggressiveBidders.Add(bidder.Username);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rileva situazione di "duello" (solo 2 bidder attivi che si contendono l'asta)
|
|
/// In questa situazione bisogna essere pronti perché se uno si ritira l'altro vince
|
|
/// </summary>
|
|
private void DetectDuelSituation(AuctionInfo auction, AppSettings settings, string currentUsername)
|
|
{
|
|
var windowSize = settings.DuelDetectionWindowSize > 0 ? settings.DuelDetectionWindowSize : 20;
|
|
var recentWindow = auction.RecentBids.Take(windowSize).ToList();
|
|
|
|
if (recentWindow.Count < 6) // Serve un minimo di puntate per rilevare un pattern
|
|
{
|
|
auction.IsDuelSituation = false;
|
|
auction.DuelOpponent = null;
|
|
return;
|
|
}
|
|
|
|
var bidders = recentWindow
|
|
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
|
.Select(g => new { Username = g.Key, Count = g.Count(), Percentage = (double)g.Count() / recentWindow.Count * 100 })
|
|
.OrderByDescending(b => b.Count)
|
|
.ToList();
|
|
|
|
// Duello: esattamente 2 bidder dominanti che coprono almeno l'80% delle puntate
|
|
if (bidders.Count >= 2)
|
|
{
|
|
var top2Percentage = bidders.Take(2).Sum(b => b.Percentage);
|
|
|
|
if (top2Percentage >= 80 && bidders.Count <= 3)
|
|
{
|
|
auction.IsDuelSituation = true;
|
|
|
|
// Trova l'avversario (chi NON sono io)
|
|
var opponent = bidders.FirstOrDefault(b =>
|
|
!b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
|
|
|
|
auction.DuelOpponent = opponent?.Username;
|
|
|
|
// Calcola chi sta dominando
|
|
var myStats = bidders.FirstOrDefault(b =>
|
|
b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
|
|
|
|
auction.DuelAdvantage = myStats != null && opponent != null
|
|
? myStats.Percentage - opponent.Percentage
|
|
: 0;
|
|
}
|
|
else
|
|
{
|
|
auction.IsDuelSituation = false;
|
|
auction.DuelOpponent = null;
|
|
auction.DuelAdvantage = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auction.IsDuelSituation = false;
|
|
auction.DuelOpponent = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica se è il caso di puntare considerando tutte le strategie
|
|
/// </summary>
|
|
public BidDecision ShouldPlaceBid(AuctionInfo auction, AuctionState state, AppSettings settings, string currentUsername)
|
|
{
|
|
var decision = new BidDecision { ShouldBid = true };
|
|
|
|
// Se le strategie avanzate sono disabilitate per questa asta, salta tutto
|
|
if (auction.AdvancedStrategiesEnabled == false)
|
|
{
|
|
return decision;
|
|
}
|
|
|
|
// ?? 1. ENTRY POINT - Verifica se il prezzo è conveniente
|
|
// Punta solo se prezzo < (MaxPrice * 0.7)
|
|
if (settings.EntryPointEnabled && auction.MaxPrice > 0)
|
|
{
|
|
var entryThreshold = auction.MaxPrice * 0.7;
|
|
if (state.Price >= entryThreshold)
|
|
{
|
|
decision.ShouldBid = false;
|
|
decision.Reason = $"Entry point: €{state.Price:F2} >= 70% di max €{auction.MaxPrice:F2}";
|
|
return decision;
|
|
}
|
|
}
|
|
|
|
// ?? 2. ANTI-BOT - Rileva pattern bot (timing identico)
|
|
if (settings.AntiBotDetectionEnabled && !string.IsNullOrEmpty(state.LastBidder))
|
|
{
|
|
var botCheck = DetectBotPattern(auction, state.LastBidder, currentUsername);
|
|
if (botCheck.IsBot)
|
|
{
|
|
decision.ShouldBid = false;
|
|
decision.Reason = $"Anti-bot: {state.LastBidder} pattern sospetto (var={botCheck.TimingVarianceMs:F0}ms)";
|
|
return decision;
|
|
}
|
|
}
|
|
|
|
// ?? 3. USER EXHAUSTION - Sfrutta utenti stanchi (info solo, non blocca)
|
|
if (settings.UserExhaustionEnabled && !string.IsNullOrEmpty(state.LastBidder))
|
|
{
|
|
var exhaustionCheck = CheckUserExhaustion(auction, state.LastBidder, currentUsername);
|
|
// Non blocchiamo, ma potremmo loggare per info
|
|
}
|
|
|
|
// 4. Verifica soft retreat
|
|
if (settings.SoftRetreatEnabled || (auction.SoftRetreatEnabledOverride ?? settings.SoftRetreatEnabled))
|
|
{
|
|
if (auction.IsInSoftRetreat)
|
|
{
|
|
var retreatEnd = auction.LastSoftRetreatAt?.AddSeconds(settings.SoftRetreatDurationSeconds);
|
|
if (retreatEnd > DateTime.UtcNow)
|
|
{
|
|
decision.ShouldBid = false;
|
|
decision.Reason = $"Soft retreat attivo (termina tra {(retreatEnd.Value - DateTime.UtcNow).TotalSeconds:F0}s)";
|
|
return decision;
|
|
}
|
|
else
|
|
{
|
|
// Fine soft retreat
|
|
auction.IsInSoftRetreat = false;
|
|
auction.ConsecutiveCollisions = 0;
|
|
}
|
|
}
|
|
|
|
// Verifica se attivare soft retreat
|
|
if (auction.ConsecutiveCollisions >= settings.SoftRetreatAfterCollisions)
|
|
{
|
|
auction.IsInSoftRetreat = true;
|
|
auction.LastSoftRetreatAt = DateTime.UtcNow;
|
|
decision.ShouldBid = false;
|
|
decision.Reason = $"Soft retreat attivato dopo {auction.ConsecutiveCollisions} collisioni";
|
|
return decision;
|
|
}
|
|
}
|
|
|
|
// 2. Verifica competition threshold
|
|
if (settings.CompetitionDetectionEnabled)
|
|
{
|
|
if (auction.ActiveBiddersCount >= settings.CompetitionThreshold)
|
|
{
|
|
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
|
var lastBid = auction.RecentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
|
if (lastBid != null && !lastBid.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (settings.AutoPauseHotAuctions && auction.HeatMetric >= settings.HeatThresholdForPause)
|
|
{
|
|
decision.ShouldBid = false;
|
|
decision.Reason = $"Asta troppo calda (heat={auction.HeatMetric}%, bidder={auction.ActiveBiddersCount})";
|
|
return decision;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Verifica opponent profiling
|
|
if (settings.OpponentProfilingEnabled && auction.AggressiveBidders.Count > 0)
|
|
{
|
|
if (settings.AggressiveBidderAction == "Avoid")
|
|
{
|
|
decision.ShouldBid = false;
|
|
decision.Reason = $"Bidder aggressivi rilevati: {string.Join(", ", auction.AggressiveBidders.Take(3))}";
|
|
return decision;
|
|
}
|
|
}
|
|
|
|
// 4. Probabilistic bidding
|
|
if (settings.ProbabilisticBiddingEnabled)
|
|
{
|
|
var probability = CalculateBidProbability(auction, settings);
|
|
var roll = _random.NextDouble();
|
|
|
|
if (roll > probability)
|
|
{
|
|
decision.ShouldBid = false;
|
|
decision.Reason = $"Skip probabilistico (p={probability:P0}, roll={roll:P0})";
|
|
return decision;
|
|
}
|
|
}
|
|
|
|
// 5. Bankroll manager
|
|
if (settings.BankrollManagerEnabled)
|
|
{
|
|
var bankrollCheck = CheckBankrollLimits(auction, settings);
|
|
if (!bankrollCheck.CanBid)
|
|
{
|
|
decision.ShouldBid = false;
|
|
decision.Reason = bankrollCheck.Reason;
|
|
return decision;
|
|
}
|
|
}
|
|
|
|
return decision;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rileva pattern bot analizzando i delta timing degli ultimi bid
|
|
/// </summary>
|
|
private (bool IsBot, double TimingVarianceMs) DetectBotPattern(AuctionInfo auction, string? lastBidder, string currentUsername)
|
|
{
|
|
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
return (false, 999);
|
|
|
|
// Ottieni gli ultimi 3+ bid di questo utente
|
|
var userBids = auction.RecentBids
|
|
.Where(b => b.Username.Equals(lastBidder, StringComparison.OrdinalIgnoreCase))
|
|
.OrderByDescending(b => b.Timestamp)
|
|
.Take(4)
|
|
.ToList();
|
|
|
|
if (userBids.Count < 3)
|
|
return (false, 999);
|
|
|
|
// Calcola i delta tra bid consecutivi
|
|
var deltas = new List<long>();
|
|
for (int i = 0; i < userBids.Count - 1; i++)
|
|
{
|
|
deltas.Add(userBids[i].Timestamp - userBids[i + 1].Timestamp);
|
|
}
|
|
|
|
if (deltas.Count < 2)
|
|
return (false, 999);
|
|
|
|
// Calcola varianza dei delta
|
|
var avg = deltas.Average();
|
|
var variance = deltas.Sum(d => Math.Pow(d - avg, 2)) / deltas.Count;
|
|
var stdDev = Math.Sqrt(variance) * 1000; // Converti in ms
|
|
|
|
// Se la varianza è < 50ms, probabilmente è un bot
|
|
return (stdDev < 50, stdDev);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica se un utente è esausto (molte puntate, può mollare)
|
|
/// </summary>
|
|
private (bool ShouldExploit, string Reason) CheckUserExhaustion(AuctionInfo auction, string? lastBidder, string currentUsername)
|
|
{
|
|
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
|
return (false, "");
|
|
|
|
// Verifica se l'utente è un "heavy user" (>50 puntate totali)
|
|
if (auction.BidderStats.TryGetValue(lastBidder, out var stats))
|
|
{
|
|
if (stats.BidCount > 50)
|
|
{
|
|
// Se ci sono pochi altri bidder attivi, può essere un buon momento
|
|
var activeBidders = auction.BidderStats.Values.Count(b => b.BidCount > 5);
|
|
if (activeBidders <= 3)
|
|
{
|
|
return (true, $"{lastBidder} ha {stats.BidCount} puntate, potrebbe mollare");
|
|
}
|
|
}
|
|
}
|
|
|
|
return (false, "");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calcola probabilità di puntata basata su competizione e ROI
|
|
/// </summary>
|
|
private double CalculateBidProbability(AuctionInfo auction, AppSettings settings)
|
|
{
|
|
var probability = settings.BaseBidProbability;
|
|
|
|
// Riduci probabilità per ogni bidder attivo oltre la soglia
|
|
var extraBidders = Math.Max(0, auction.ActiveBiddersCount - settings.CompetitionThreshold);
|
|
probability -= extraBidders * settings.ProbabilityReductionPerBidder;
|
|
|
|
// Riduci per heat metric alto
|
|
if (auction.HeatMetric > 70)
|
|
{
|
|
probability -= 0.1;
|
|
}
|
|
|
|
// Aumenta se abbiamo un buon ROI potenziale
|
|
if (auction.CalculatedValue?.Savings > 0)
|
|
{
|
|
probability += 0.1;
|
|
}
|
|
|
|
return Math.Clamp(probability, 0.1, 1.0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica limiti bankroll
|
|
/// </summary>
|
|
private BankrollCheckResult CheckBankrollLimits(AuctionInfo auction, AppSettings settings)
|
|
{
|
|
var result = new BankrollCheckResult { CanBid = true };
|
|
|
|
// Limite puntate per asta
|
|
var maxPerAuction = auction.MaxBidsOverride ?? settings.MaxBidsPerAuction;
|
|
if (maxPerAuction > 0 && auction.SessionBidCount >= maxPerAuction)
|
|
{
|
|
result.CanBid = false;
|
|
result.Reason = $"Limite puntate per asta raggiunto ({auction.SessionBidCount}/{maxPerAuction})";
|
|
return result;
|
|
}
|
|
|
|
// Limite puntate per sessione
|
|
if (settings.MaxBidsPerSession > 0 && _sessionTotalBids >= settings.MaxBidsPerSession)
|
|
{
|
|
result.CanBid = false;
|
|
result.Reason = $"Limite puntate per sessione raggiunto ({_sessionTotalBids}/{settings.MaxBidsPerSession})";
|
|
return result;
|
|
}
|
|
|
|
// Budget giornaliero
|
|
if (settings.DailyBudgetEuro > 0)
|
|
{
|
|
var spent = _sessionTotalBids * settings.AverageBidCostEuro;
|
|
if (spent >= settings.DailyBudgetEuro)
|
|
{
|
|
result.CanBid = false;
|
|
result.Reason = $"Budget giornaliero esaurito (€{spent:F2}/€{settings.DailyBudgetEuro:F2})";
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registra una puntata effettuata (per tracking)
|
|
/// </summary>
|
|
public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false)
|
|
{
|
|
auction.SessionBidCount++;
|
|
_sessionTotalBids++;
|
|
|
|
if (success)
|
|
{
|
|
auction.SuccessfulBidCount++;
|
|
auction.ConsecutiveCollisions = 0;
|
|
}
|
|
else
|
|
{
|
|
auction.FailedBidCount++;
|
|
}
|
|
|
|
if (collision)
|
|
{
|
|
auction.CollisionCount++;
|
|
auction.ConsecutiveCollisions++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registra un timer scaduto
|
|
/// </summary>
|
|
public void RecordTimerExpired(AuctionInfo auction)
|
|
{
|
|
auction.TimerExpiredCount++;
|
|
auction.ConsecutiveCollisions++; // Conta come "mancato"
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset contatori sessione
|
|
/// </summary>
|
|
public void ResetSession()
|
|
{
|
|
_sessionTotalBids = 0;
|
|
_sessionStartedAt = DateTime.UtcNow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene statistiche sessione corrente
|
|
/// </summary>
|
|
public SessionStats GetSessionStats()
|
|
{
|
|
return new SessionStats
|
|
{
|
|
TotalBids = _sessionTotalBids,
|
|
SessionDuration = DateTime.UtcNow - _sessionStartedAt
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Risultato calcolo timing puntata
|
|
/// </summary>
|
|
public class BidTimingResult
|
|
{
|
|
public int BaseOffsetMs { get; set; }
|
|
public int LatencyCompensationMs { get; set; }
|
|
public int DynamicAdjustmentMs { get; set; }
|
|
public int JitterMs { get; set; }
|
|
public int FinalOffsetMs { get; set; }
|
|
public bool ShouldBid { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decisione se puntare
|
|
/// </summary>
|
|
public class BidDecision
|
|
{
|
|
public bool ShouldBid { get; set; }
|
|
public string? Reason { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Risultato verifica bankroll
|
|
/// </summary>
|
|
public class BankrollCheckResult
|
|
{
|
|
public bool CanBid { get; set; }
|
|
public string? Reason { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Statistiche sessione
|
|
/// </summary>
|
|
public class SessionStats
|
|
{
|
|
public int TotalBids { get; set; }
|
|
public TimeSpan SessionDuration { get; set; }
|
|
}
|
|
}
|