Files
Mimante/Mimante/Engine/Backtest/DossierReader.cs
T
Alby96 7ca504a70a Add utility classes for theme management, waiting, watched products, and notifications
- 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.
2026-08-04 21:49:53 +02:00

219 lines
8.5 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}