Phase 0: UTC-Datumsauswertung korrigiert, Portierungsanalyse und -plan
Vorarbeiten fuer die Linux-/NET-10-Portierung (siehe docs/PLAN-Linux-Portierung.md). DateTime.TryParse ohne DateTimeStyles liefert bei Z-Zeitstempeln Kind=Local und rechnet in Lokalzeit um. An 13 Stellen in PolymarketProvider und LimitlessProvider wurden so ISO-Zeitstempel der Plattform-APIs eingelesen, waehrend der Fallback derselben Zuweisungen (DateTime.UtcNow) korrektes UTC schrieb - die Spalten enthielten also gemischt verschobene und korrekte Werte. Auf einem Server mit TZ=UTC haette derselbe Code andere Werte erzeugt als unter Windows. Neuer Helfer Infrastructure/Helpers/DateParsing.cs kapselt das Parsen mit InvariantCulture und AdjustToUniversal|AssumeUniversal; alle Fundstellen umgestellt. Ausserdem: - LimitlessProvider: Volume-Parsing auf InvariantCulture umgestellt - EmbeddedWebServer: hartkodierten j:\-Absolutpfad als wwwroot-Fallback entfernt Ein Backfill der Altdaten erfolgt bewusst nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses timestamp strings from platform APIs as UTC.
|
||||
/// <para>
|
||||
/// The platforms deliver ISO-8601 strings; the domain stores UTC throughout — every call
|
||||
/// site falls back to <see cref="DateTime.UtcNow"/>, and the neighbouring DbCreatedAt is
|
||||
/// always UTC. A plain <c>DateTime.TryParse</c> breaks that in two ways: it honours the
|
||||
/// machine's locale, and it converts a "Z"-suffixed timestamp to <em>local</em> time. The
|
||||
/// same API payload would therefore land in the database differently on a Berlin
|
||||
/// workstation than on a UTC server.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class DateParsing
|
||||
{
|
||||
private const DateTimeStyles UtcStyles =
|
||||
DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an API timestamp as UTC. Returns null when absent or unparseable.
|
||||
/// Timestamps without a zone designator are treated as UTC.
|
||||
/// </summary>
|
||||
public static DateTime? ParseUtc(string? value) =>
|
||||
DateTime.TryParse(value, CultureInfo.InvariantCulture, UtcStyles, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an API timestamp as UTC, falling back to the current UTC time.
|
||||
/// </summary>
|
||||
public static DateTime ParseUtcOrNow(string? value) => ParseUtc(value) ?? DateTime.UtcNow;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Globalization;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Helpers;
|
||||
using Predictalytics.Infrastructure.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -235,7 +237,7 @@ public class LimitlessProvider : IPlatformProvider
|
||||
Price = (decimal)(e.Price ?? 0),
|
||||
Size = (decimal)(e.Size ?? 0),
|
||||
Amount = (decimal)(e.Price * e.Size ?? 0),
|
||||
ExecutedAt = DateTime.TryParse(e.CreatedAt, out var dt) ? dt : DateTime.UtcNow,
|
||||
ExecutedAt = DateParsing.ParseUtcOrNow(e.CreatedAt),
|
||||
TransactionHash = e.TxHash,
|
||||
TransientWallet = wallet,
|
||||
};
|
||||
@@ -265,11 +267,12 @@ public class LimitlessProvider : IPlatformProvider
|
||||
Category = catMap.Category,
|
||||
Subcategory = catMap.Subcategory,
|
||||
ImageUrl = raw.ImageUrl ?? "",
|
||||
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""), out var vol) ? vol : 0,
|
||||
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""),
|
||||
NumberStyles.Any, CultureInfo.InvariantCulture, out var vol) ? vol : 0,
|
||||
Liquidity = (decimal)(raw.Liquidity ?? 0),
|
||||
EndDate = (raw.ExpirationTimestamp.HasValue && raw.ExpirationTimestamp.Value > 0 && raw.ExpirationTimestamp.Value < 253402300799)
|
||||
? DateTimeOffset.FromUnixTimeSeconds(raw.ExpirationTimestamp.Value).UtcDateTime
|
||||
: (DateTime.TryParse(raw.ExpirationDate, out var ed) ? ed : null),
|
||||
? DateTimeOffset.FromUnixTimeSeconds(raw.ExpirationTimestamp.Value).UtcDateTime
|
||||
: DateParsing.ParseUtc(raw.ExpirationDate),
|
||||
IsResolved = raw.Closed,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Text.Json;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Helpers;
|
||||
using Predictalytics.Infrastructure.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -216,9 +217,9 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Title = rawEv.Title,
|
||||
Description = rawEv.Description,
|
||||
ImageUrl = rawEv.Image,
|
||||
StartDate = DateTime.TryParse(rawEv.StartDate, out var esd) ? esd : null,
|
||||
EndDate = DateTime.TryParse(rawEv.EndDate, out var eed) ? eed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEv.CreatedAt, out var ecd) ? ecd : DateTime.UtcNow,
|
||||
StartDate = DateParsing.ParseUtc(rawEv.StartDate),
|
||||
EndDate = DateParsing.ParseUtc(rawEv.EndDate),
|
||||
CreatedAt = DateParsing.ParseUtcOrNow(rawEv.CreatedAt),
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEv.Active,
|
||||
IsClosed = rawEv.Closed,
|
||||
@@ -312,9 +313,9 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Title = rawEvent.Title,
|
||||
Description = rawEvent.Description,
|
||||
ImageUrl = rawEvent.Image,
|
||||
StartDate = DateTime.TryParse(rawEvent.StartDate, out var sd) ? sd : null,
|
||||
EndDate = DateTime.TryParse(rawEvent.EndDate, out var ed) ? ed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEvent.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
|
||||
StartDate = DateParsing.ParseUtc(rawEvent.StartDate),
|
||||
EndDate = DateParsing.ParseUtc(rawEvent.EndDate),
|
||||
CreatedAt = DateParsing.ParseUtcOrNow(rawEvent.CreatedAt),
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEvent.Active,
|
||||
IsClosed = rawEvent.Closed,
|
||||
@@ -359,14 +360,14 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Volume = (decimal)raw.Volume,
|
||||
Volume24h = (decimal)raw.Volume24hr,
|
||||
Liquidity = (decimal)raw.Liquidity,
|
||||
StartDate = DateTime.TryParse(raw.StartDate, out var msd) ? msd : null,
|
||||
EndDate = DateTime.TryParse(raw.EndDate ?? raw.EndDateIso, out var med) ? med : null,
|
||||
CreatedAt = DateTime.TryParse(raw.CreatedAt, out var mcd) ? mcd : DateTime.UtcNow,
|
||||
StartDate = DateParsing.ParseUtc(raw.StartDate),
|
||||
EndDate = DateParsing.ParseUtc(raw.EndDate ?? raw.EndDateIso),
|
||||
CreatedAt = DateParsing.ParseUtcOrNow(raw.CreatedAt),
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
LastUpdatedAt = DateTime.UtcNow,
|
||||
FeeRateBps = (decimal)(raw.TakerFee * 10000),
|
||||
IsNegRisk = raw.NegRisk,
|
||||
ClosedAt = DateTime.TryParse(raw.ClosedTime, out var mct) ? mct : null
|
||||
ClosedAt = DateParsing.ParseUtc(raw.ClosedTime)
|
||||
};
|
||||
|
||||
// Parse outcomes, prices, and token IDs from JSON strings
|
||||
|
||||
@@ -315,9 +315,7 @@ public class EmbeddedWebServer
|
||||
Path.Combine(baseDir, "wwwroot"),
|
||||
// From bin/Debug/net8.0-windows/ up to src/, then into Api/wwwroot
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "Predictalytics.Api", "wwwroot")),
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "src", "Predictalytics.Api", "wwwroot")),
|
||||
// Absolute fallback for this specific workspace
|
||||
@"j:\Softwareprojekte\Predictalytics\Predictalytics\src\Predictalytics.Api\wwwroot"
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "src", "Predictalytics.Api", "wwwroot"))
|
||||
};
|
||||
|
||||
foreach (var path in candidates)
|
||||
|
||||
Reference in New Issue
Block a user