110 lines
3.4 KiB
C#
110 lines
3.4 KiB
C#
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));
|
||
}
|
||
}
|
||
} |