- Implement ThemeManager for dynamic light/dark theme switching in the application. - Create Wait class for cancellable delays without exceptions for smoother user experience. - Introduce WatchedProductsStore to manage and persist watched products in JSON format. - Add WindowsNotifier for system notifications to inform users of important events. - Develop ProductViewModel to encapsulate product data and manage UI interactions effectively.
219 lines
8.5 KiB
C#
219 lines
8.5 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Text.Json;
|
||
|
||
namespace AutoBidder.Engine.Backtest
|
||
{
|
||
/// <summary>
|
||
/// Legge un dossier d'asta (JSON Lines) e lo riporta in memoria come sequenza di
|
||
/// eventi tipizzati.
|
||
///
|
||
/// <para>È volutamente separato da chi lo consuma e non conosce il motore: il dossier
|
||
/// è un formato scritto altrove e cambierà, quindi la sua interpretazione va tenuta in
|
||
/// un punto solo e verificabile su righe scritte a mano.</para>
|
||
///
|
||
/// <para>Una riga che non si capisce viene <b>saltata</b>, non fa fallire la lettura:
|
||
/// i dossier vengono chiusi dal processo mentre gira, quindi l'ultima riga di un file
|
||
/// interrotto può benissimo essere monca — e sarebbe assurdo buttare via otto ore di
|
||
/// osservazione per l'ultimo mezzo evento.</para>
|
||
/// </summary>
|
||
public static class DossierReader
|
||
{
|
||
/// <summary>Intestazione: identità dell'asta e configurazione con cui era seguita.</summary>
|
||
public sealed record Header(
|
||
string AuctionId,
|
||
string Name,
|
||
string ProductKey,
|
||
double? BuyNowPrice,
|
||
double BidCostEuro,
|
||
int ConfiguredLeadMs,
|
||
string State);
|
||
|
||
/// <summary>Una singola interrogazione a data.php.</summary>
|
||
public sealed record Poll(
|
||
double T,
|
||
double Price,
|
||
double Timer,
|
||
string Status,
|
||
string LastBidder,
|
||
bool Mine,
|
||
long ExpiryUnix,
|
||
long ServerUnix,
|
||
double PingMs);
|
||
|
||
/// <summary>Una puntata altrui (o nostra) letta dallo storico.</summary>
|
||
public sealed record Bid(
|
||
double T,
|
||
string User,
|
||
long UnixSeconds,
|
||
double Price,
|
||
bool Mine);
|
||
|
||
/// <summary>Riepilogo scritto alla chiusura dell'asta.</summary>
|
||
public sealed record Summary(
|
||
string Outcome,
|
||
string Winner,
|
||
double FinalPrice,
|
||
bool WonByMe,
|
||
int Resets,
|
||
int DistinctBidders,
|
||
int MyBids);
|
||
|
||
/// <summary>Un dossier completo.</summary>
|
||
public sealed class Session
|
||
{
|
||
public Header? Header { get; init; }
|
||
public Summary? Summary { get; init; }
|
||
public List<Poll> Polls { get; init; } = new();
|
||
public List<Bid> Bids { get; init; } = new();
|
||
|
||
/// <summary>Righe che non si è riusciti a interpretare.</summary>
|
||
public int SkippedLines { get; init; }
|
||
|
||
/// <summary>Un dossier senza intestazione non dice di che asta parla.</summary>
|
||
public bool IsUsable => Header != null && Polls.Count > 0;
|
||
}
|
||
|
||
public static Session ReadFile(string path) => Read(File.ReadLines(path));
|
||
|
||
public static Session Read(IEnumerable<string> lines)
|
||
{
|
||
Header? header = null;
|
||
Summary? summary = null;
|
||
var polls = new List<Poll>();
|
||
var bids = new List<Bid>();
|
||
var skipped = 0;
|
||
|
||
foreach (var raw in lines)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(raw)) continue;
|
||
|
||
// Il primo file scritto porta il segnaposto di codifica in testa.
|
||
var line = raw.TrimStart('', ' ', '\t');
|
||
if (line.Length == 0 || line[0] != '{') { skipped++; continue; }
|
||
|
||
try
|
||
{
|
||
using var doc = JsonDocument.Parse(line);
|
||
var root = doc.RootElement;
|
||
|
||
switch (Str(root, "type"))
|
||
{
|
||
case "header": header = ReadHeader(root); break;
|
||
case "summary": summary = ReadSummary(root); break;
|
||
case "poll": polls.Add(ReadPoll(root)); break;
|
||
case "bid": bids.Add(ReadBid(root)); break;
|
||
// reset e log non servono alla simulazione: il reset è deducibile
|
||
// dal cambio di scadenza, e il log è testo per l'utente.
|
||
}
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
skipped++;
|
||
}
|
||
}
|
||
|
||
return new Session
|
||
{
|
||
Header = header,
|
||
Summary = summary,
|
||
Polls = polls,
|
||
Bids = bids,
|
||
SkippedLines = skipped
|
||
};
|
||
}
|
||
|
||
// ── Lettura dei singoli eventi ───────────────────────────────────
|
||
|
||
private static Header ReadHeader(JsonElement root)
|
||
{
|
||
var product = Obj(root, "product");
|
||
var config = Obj(root, "config");
|
||
|
||
return new Header(
|
||
AuctionId: Str(root, "auctionId") ?? "",
|
||
Name: Str(root, "name") ?? "",
|
||
ProductKey: Str(root, "productKey") ?? "",
|
||
BuyNowPrice: product.HasValue ? NullableNum(product.Value, "buyNowPrice") : null,
|
||
BidCostEuro: product.HasValue ? Num(product.Value, "bidCostEuro", 0.20) : 0.20,
|
||
ConfiguredLeadMs: config.HasValue ? (int)Num(config.Value, "bidBeforeDeadlineMs", 0) : 0,
|
||
State: config.HasValue ? Str(config.Value, "state") ?? "" : "");
|
||
}
|
||
|
||
private static Summary ReadSummary(JsonElement root)
|
||
{
|
||
var part = Obj(root, "participation");
|
||
|
||
return new Summary(
|
||
Outcome: Str(root, "outcome") ?? "",
|
||
Winner: Str(root, "winner") ?? "",
|
||
FinalPrice: Num(root, "finalPrice", 0),
|
||
WonByMe: Bool(root, "wonByMe"),
|
||
Resets: part.HasValue ? (int)Num(part.Value, "resets", 0) : 0,
|
||
DistinctBidders: part.HasValue ? (int)Num(part.Value, "distinctBidders", 0) : 0,
|
||
MyBids: part.HasValue ? (int)Num(part.Value, "myBids", 0) : 0);
|
||
}
|
||
|
||
private static Poll ReadPoll(JsonElement root) => new(
|
||
T: Num(root, "t", 0),
|
||
Price: Num(root, "price", 0),
|
||
Timer: Num(root, "timer", 0),
|
||
Status: Str(root, "status") ?? "",
|
||
LastBidder: Str(root, "lastBidder") ?? "",
|
||
Mine: Bool(root, "mine"),
|
||
ExpiryUnix: (long)Num(root, "expiryUnix", 0),
|
||
ServerUnix: (long)Num(root, "serverUnix", 0),
|
||
PingMs: Num(root, "pingMs", 0));
|
||
|
||
private static Bid ReadBid(JsonElement root) => new(
|
||
T: Num(root, "t", 0),
|
||
User: Str(root, "user") ?? "",
|
||
UnixSeconds: ParseUnix(Str(root, "bidAt")),
|
||
Price: Num(root, "price", 0),
|
||
Mine: Bool(root, "mine"));
|
||
|
||
/// <summary>
|
||
/// Le puntate portano l'istante come data ISO con fuso. Serve il secondo unix,
|
||
/// perché è nella stessa unità dei timestamp che il motore vede a runtime.
|
||
/// </summary>
|
||
private static long ParseUnix(string? iso)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(iso)) return 0;
|
||
|
||
return DateTimeOffset.TryParse(iso, CultureInfo.InvariantCulture,
|
||
DateTimeStyles.RoundtripKind, out var parsed)
|
||
? parsed.ToUnixTimeSeconds()
|
||
: 0;
|
||
}
|
||
|
||
// ── Accessori tolleranti ─────────────────────────────────────────
|
||
|
||
private static string? Str(JsonElement owner, string name) =>
|
||
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
|
||
? v.GetString()
|
||
: null;
|
||
|
||
private static bool Bool(JsonElement owner, string name) =>
|
||
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.True;
|
||
|
||
private static double Num(JsonElement owner, string name, double fallback) =>
|
||
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number &&
|
||
v.TryGetDouble(out var d)
|
||
? d
|
||
: fallback;
|
||
|
||
private static double? NullableNum(JsonElement owner, string name) =>
|
||
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number &&
|
||
v.TryGetDouble(out var d)
|
||
? d
|
||
: null;
|
||
|
||
private static JsonElement? Obj(JsonElement owner, string name) =>
|
||
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Object
|
||
? v
|
||
: null;
|
||
}
|
||
}
|