PaperTradingClock.cs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. using Domain.Entities.Paper.ValueObject;
  2. namespace Application.Helpers;
  3. /// <summary>
  4. /// 모의투자 체결 규칙(KST) 계산 — 접수 시각에 따라 FillRule/TargetDate/CancelableUntil 을 확정한다 (d4 §③).
  5. /// • 영업일 t &lt; 09:00 → 당일 시가 (CancelableUntil = 당일 09:00)
  6. /// • 영업일 09:00 ≤ t &lt; 15:30 → 당일 종가 (CancelableUntil = 당일 15:30)
  7. /// • t ≥ 15:30 또는 휴장일 → 다음 영업일 시가 (CancelableUntil = 익영업일 09:00)
  8. /// 휴장일은 주말 + 호출자가 주입하는 MarketHoliday 집합으로 판정한다.
  9. /// </summary>
  10. public static class PaperTradingClock
  11. {
  12. private static readonly TimeOnly MarketOpen = new(9, 0);
  13. private static readonly TimeOnly MarketClose = new(15, 30);
  14. private static readonly TimeZoneInfo Kst = ResolveKst();
  15. public static DateTime NowKst() => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Kst);
  16. public static DateTime ToUtc(DateTime kst)
  17. {
  18. var unspecified = DateTime.SpecifyKind(kst, DateTimeKind.Unspecified);
  19. return TimeZoneInfo.ConvertTimeToUtc(unspecified, Kst);
  20. }
  21. public static bool IsHoliday(DateOnly date, IReadOnlySet<DateOnly> holidays)
  22. {
  23. if (date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
  24. {
  25. return true;
  26. }
  27. return holidays.Contains(date);
  28. }
  29. /// <summary>date 이후(자기 자신 제외)의 다음 영업일.</summary>
  30. public static DateOnly NextBusinessDay(DateOnly date, IReadOnlySet<DateOnly> holidays)
  31. {
  32. var next = date.AddDays(1);
  33. while (IsHoliday(next, holidays))
  34. {
  35. next = next.AddDays(1);
  36. }
  37. return next;
  38. }
  39. /// <summary>date 가 영업일이면 그대로, 아니면 다음 영업일.</summary>
  40. public static DateOnly EnsureBusinessDay(DateOnly date, IReadOnlySet<DateOnly> holidays)
  41. {
  42. var d = date;
  43. while (IsHoliday(d, holidays))
  44. {
  45. d = d.AddDays(1);
  46. }
  47. return d;
  48. }
  49. /// <summary>
  50. /// 접수 시각(KST)으로 체결 계획을 산정한다. 반환은 (FillRule, TargetDate, CancelableUntilKst).
  51. /// CancelableUntilKst 는 KST 벽시계 기준 — 호출자가 ToUtc 로 변환해 저장한다.
  52. /// </summary>
  53. public static (PaperFillRule FillRule, DateOnly TargetDate, DateTime CancelableUntilKst) Plan(
  54. DateTime nowKst,
  55. IReadOnlySet<DateOnly> holidays)
  56. {
  57. var today = DateOnly.FromDateTime(nowKst);
  58. var timeOfDay = TimeOnly.FromDateTime(nowKst);
  59. if (!IsHoliday(today, holidays))
  60. {
  61. if (timeOfDay < MarketOpen)
  62. {
  63. // 당일 시가
  64. var cancelable = today.ToDateTime(MarketOpen);
  65. return (PaperFillRule.Open, today, cancelable);
  66. }
  67. if (timeOfDay < MarketClose)
  68. {
  69. // 당일 종가
  70. var cancelable = today.ToDateTime(MarketClose);
  71. return (PaperFillRule.Close, today, cancelable);
  72. }
  73. }
  74. // 15:30 이후 또는 휴장일 → 다음 영업일 시가
  75. var nextDay = NextBusinessDay(today, holidays);
  76. var cancelableUntil = nextDay.ToDateTime(MarketOpen);
  77. return (PaperFillRule.Open, nextDay, cancelableUntil);
  78. }
  79. private static TimeZoneInfo ResolveKst()
  80. {
  81. if (TimeZoneInfo.TryFindSystemTimeZoneById("Asia/Seoul", out var tz))
  82. {
  83. return tz;
  84. }
  85. if (TimeZoneInfo.TryFindSystemTimeZoneById("Korea Standard Time", out tz))
  86. {
  87. return tz;
  88. }
  89. // KST 는 DST 없음 — 고정 +9 fallback
  90. return TimeZoneInfo.CreateCustomTimeZone("KST", TimeSpan.FromHours(9), "KST", "KST");
  91. }
  92. }