SoftTraderBackend/DatabaseHandler/HistoricalRequestParser.cs
2026-03-20 10:42:48 -04:00

110 lines
3.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.OneDay => 86400,
HistoricalBarSize.FiveDays => 432000,
HistoricalBarSize.OneMonth => 2592000,
_ => throw new ArgumentOutOfRangeException(nameof(barSize))
};
// Markets trade ~6.5 hours/day, 5 days/week — not 24/7 calendar time.
// Intraday fraction: (6.5h × 5 days) / (24h × 7 days) ≈ 0.194
// Daily/weekly fraction (weekdays only): 5 / 7 ≈ 0.714
// Monthly bars already map 1:1 to calendar months, no adjustment needed.
// NYSE has ~252 trading days per year (weekdays minus ~10 holidays).
// Using 252/365 instead of 5/7 avoids overestimating for 3+ month windows
// where accumulated holidays cause the cache check to fail.
double marketFraction = barSize switch
{
HistoricalBarSize.OneMonth => 1.0,
HistoricalBarSize.OneDay or HistoricalBarSize.FiveDays => 252.0 / 365.0,
_ => (6.5 * 252.0) / (24.0 * 365.0)
};
return Math.Max(1, (int)Math.Floor(duration.TotalSeconds * marketFraction / secondsPerBar));
}
}
}