using Domain.Entities.Paper.ValueObject;
namespace Application.Helpers;
///
/// 모의투자 체결 규칙(KST) 계산 — 접수 시각에 따라 FillRule/TargetDate/CancelableUntil 을 확정한다 (d4 §③).
/// • 영업일 t < 09:00 → 당일 시가 (CancelableUntil = 당일 09:00)
/// • 영업일 09:00 ≤ t < 15:30 → 당일 종가 (CancelableUntil = 당일 15:30)
/// • t ≥ 15:30 또는 휴장일 → 다음 영업일 시가 (CancelableUntil = 익영업일 09:00)
/// 휴장일은 주말 + 호출자가 주입하는 MarketHoliday 집합으로 판정한다.
///
public static class PaperTradingClock
{
private static readonly TimeOnly MarketOpen = new(9, 0);
private static readonly TimeOnly MarketClose = new(15, 30);
private static readonly TimeZoneInfo Kst = ResolveKst();
public static DateTime NowKst() => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Kst);
public static DateTime ToUtc(DateTime kst)
{
var unspecified = DateTime.SpecifyKind(kst, DateTimeKind.Unspecified);
return TimeZoneInfo.ConvertTimeToUtc(unspecified, Kst);
}
public static bool IsHoliday(DateOnly date, IReadOnlySet holidays)
{
if (date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
{
return true;
}
return holidays.Contains(date);
}
/// date 이후(자기 자신 제외)의 다음 영업일.
public static DateOnly NextBusinessDay(DateOnly date, IReadOnlySet holidays)
{
var next = date.AddDays(1);
while (IsHoliday(next, holidays))
{
next = next.AddDays(1);
}
return next;
}
/// date 가 영업일이면 그대로, 아니면 다음 영업일.
public static DateOnly EnsureBusinessDay(DateOnly date, IReadOnlySet holidays)
{
var d = date;
while (IsHoliday(d, holidays))
{
d = d.AddDays(1);
}
return d;
}
///
/// 접수 시각(KST)으로 체결 계획을 산정한다. 반환은 (FillRule, TargetDate, CancelableUntilKst).
/// CancelableUntilKst 는 KST 벽시계 기준 — 호출자가 ToUtc 로 변환해 저장한다.
///
public static (PaperFillRule FillRule, DateOnly TargetDate, DateTime CancelableUntilKst) Plan(
DateTime nowKst,
IReadOnlySet holidays)
{
var today = DateOnly.FromDateTime(nowKst);
var timeOfDay = TimeOnly.FromDateTime(nowKst);
if (!IsHoliday(today, holidays))
{
if (timeOfDay < MarketOpen)
{
// 당일 시가
var cancelable = today.ToDateTime(MarketOpen);
return (PaperFillRule.Open, today, cancelable);
}
if (timeOfDay < MarketClose)
{
// 당일 종가
var cancelable = today.ToDateTime(MarketClose);
return (PaperFillRule.Close, today, cancelable);
}
}
// 15:30 이후 또는 휴장일 → 다음 영업일 시가
var nextDay = NextBusinessDay(today, holidays);
var cancelableUntil = nextDay.ToDateTime(MarketOpen);
return (PaperFillRule.Open, nextDay, cancelableUntil);
}
private static TimeZoneInfo ResolveKst()
{
if (TimeZoneInfo.TryFindSystemTimeZoneById("Asia/Seoul", out var tz))
{
return tz;
}
if (TimeZoneInfo.TryFindSystemTimeZoneById("Korea Standard Time", out tz))
{
return tz;
}
// KST 는 DST 없음 — 고정 +9 fallback
return TimeZoneInfo.CreateCustomTimeZone("KST", TimeSpan.FromHours(9), "KST", "KST");
}
}