| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108 |
- using Domain.Entities.Paper.ValueObject;
- namespace Application.Helpers;
- /// <summary>
- /// 모의투자 체결 규칙(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 집합으로 판정한다.
- /// </summary>
- 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<DateOnly> holidays)
- {
- if (date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
- {
- return true;
- }
- return holidays.Contains(date);
- }
- /// <summary>date 이후(자기 자신 제외)의 다음 영업일.</summary>
- public static DateOnly NextBusinessDay(DateOnly date, IReadOnlySet<DateOnly> holidays)
- {
- var next = date.AddDays(1);
- while (IsHoliday(next, holidays))
- {
- next = next.AddDays(1);
- }
- return next;
- }
- /// <summary>date 가 영업일이면 그대로, 아니면 다음 영업일.</summary>
- public static DateOnly EnsureBusinessDay(DateOnly date, IReadOnlySet<DateOnly> holidays)
- {
- var d = date;
- while (IsHoliday(d, holidays))
- {
- d = d.AddDays(1);
- }
- return d;
- }
- /// <summary>
- /// 접수 시각(KST)으로 체결 계획을 산정한다. 반환은 (FillRule, TargetDate, CancelableUntilKst).
- /// CancelableUntilKst 는 KST 벽시계 기준 — 호출자가 ToUtc 로 변환해 저장한다.
- /// </summary>
- public static (PaperFillRule FillRule, DateOnly TargetDate, DateTime CancelableUntilKst) Plan(
- DateTime nowKst,
- IReadOnlySet<DateOnly> 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");
- }
- }
|