using Backend.MarketDataRequest; using System.Globalization; namespace Backend.DatabaseHandler { public static class HistoricalRequestParser { public static HistoricalRequestWindow BuildWindow( string endDateTimeIsoUtc, string duration, HistoricalBarSize barSize) { DateTime endUtc = ParseRequestEndDateTimeUtc(endDateTimeIsoUtc); TimeSpan durationSpan = ParseDuration(duration); DateTime startUtc = endUtc - durationSpan; int expectedBarsMin = EstimateExpectedBars(durationSpan, barSize); return new HistoricalRequestWindow { StartUtc = startUtc, EndUtc = endUtc, ExpectedBarCountMin = expectedBarsMin }; } public static DateTime ParseRequestEndDateTimeUtc(string raw) { if (string.IsNullOrWhiteSpace(raw)) throw new FormatException("endDateTime is empty."); raw = raw.Trim(); Console.WriteLine(raw); if (!DateTimeOffset.TryParseExact( raw, new[] { "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd'T'HH:mm:ss.FFF'Z'", "O" }, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dto)) { throw new FormatException( $"endDateTime must be ISO 8601 UTC, e.g. 2026-03-17T20:00:00Z. Received: '{raw}'"); } return dto.UtcDateTime; } private static TimeSpan ParseDuration(string duration) { if (string.IsNullOrWhiteSpace(duration)) throw new ArgumentException("Duration is empty."); string normalized = duration.Trim().Replace("+", " "); var parts = normalized.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) throw new ArgumentException($"Invalid duration format: '{duration}'"); int value = int.Parse(parts[0], CultureInfo.InvariantCulture); string unit = parts[1].ToUpperInvariant(); return unit switch { "S" => TimeSpan.FromSeconds(value), "D" => TimeSpan.FromDays(value), "W" => TimeSpan.FromDays(value * 7), "M" => TimeSpan.FromDays(value * 30), "Y" => TimeSpan.FromDays(value * 365), _ => throw new ArgumentException($"Unsupported duration unit: '{unit}'") }; } private static int EstimateExpectedBars(TimeSpan duration, HistoricalBarSize barSize) { double secondsPerBar = barSize switch { HistoricalBarSize.OneMin => 60, HistoricalBarSize.FiveMins => 300, HistoricalBarSize.FifteenMins => 900, HistoricalBarSize.OneHour => 3600, HistoricalBarSize.TwoHours => 7200, HistoricalBarSize.ThreeHours => 10800, HistoricalBarSize.FourHours => 14400, HistoricalBarSize.EightHours => 28800, HistoricalBarSize.OneDay => 86400, HistoricalBarSize.FiveDays => 432000, HistoricalBarSize.OneWeek => 604800, HistoricalBarSize.OneMonth => 2592000, _ => throw new ArgumentOutOfRangeException(nameof(barSize)) }; // Markets trade ~6.5 hours/day, 5 days/week — not 24/7 calendar time. // NYSE averages ~252 trading days/year, but holiday-heavy windows // (e.g. Nov-Jan with Thanksgiving, Christmas, New Year's, MLK Day) // can have significantly fewer. Using 240/365 gives a conservative // floor that prevents false cache misses during these periods. double marketFraction = barSize switch { HistoricalBarSize.OneMonth => 1.0, HistoricalBarSize.OneDay or HistoricalBarSize.FiveDays or HistoricalBarSize.OneWeek => 240.0 / 365.0, _ => (6.5 * 240.0) / (24.0 * 365.0) }; return Math.Max(1, (int)Math.Floor(duration.TotalSeconds * marketFraction / secondsPerBar)); } } }