PaperTradingClock.cs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  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 로부터 N 영업일 뒤(자기 자신 제외). count &lt;= 0 이면 date 그대로. (예측 채점 DueDate 산정 — d2 §③)</summary>
  40. public static DateOnly AddBusinessDays(DateOnly date, int count, IReadOnlySet<DateOnly> holidays)
  41. {
  42. var result = date;
  43. for (var i = 0; i < count; i++)
  44. {
  45. result = NextBusinessDay(result, holidays);
  46. }
  47. return result;
  48. }
  49. /// <summary>date 가 영업일이면 그대로, 아니면 다음 영업일.</summary>
  50. public static DateOnly EnsureBusinessDay(DateOnly date, IReadOnlySet<DateOnly> holidays)
  51. {
  52. var d = date;
  53. while (IsHoliday(d, holidays))
  54. {
  55. d = d.AddDays(1);
  56. }
  57. return d;
  58. }
  59. /// <summary>
  60. /// 접수 시각(KST)으로 체결 계획을 산정한다. 반환은 (FillRule, TargetDate, CancelableUntilKst).
  61. /// CancelableUntilKst 는 KST 벽시계 기준 — 호출자가 ToUtc 로 변환해 저장한다.
  62. /// </summary>
  63. public static (PaperFillRule FillRule, DateOnly TargetDate, DateTime CancelableUntilKst) Plan(
  64. DateTime nowKst,
  65. IReadOnlySet<DateOnly> holidays)
  66. {
  67. var today = DateOnly.FromDateTime(nowKst);
  68. var timeOfDay = TimeOnly.FromDateTime(nowKst);
  69. if (!IsHoliday(today, holidays))
  70. {
  71. if (timeOfDay < MarketOpen)
  72. {
  73. // 당일 시가
  74. var cancelable = today.ToDateTime(MarketOpen);
  75. return (PaperFillRule.Open, today, cancelable);
  76. }
  77. if (timeOfDay < MarketClose)
  78. {
  79. // 당일 종가
  80. var cancelable = today.ToDateTime(MarketClose);
  81. return (PaperFillRule.Close, today, cancelable);
  82. }
  83. }
  84. // 15:30 이후 또는 휴장일 → 다음 영업일 시가
  85. var nextDay = NextBusinessDay(today, holidays);
  86. var cancelableUntil = nextDay.ToDateTime(MarketOpen);
  87. return (PaperFillRule.Open, nextDay, cancelableUntil);
  88. }
  89. private static TimeZoneInfo ResolveKst()
  90. {
  91. if (TimeZoneInfo.TryFindSystemTimeZoneById("Asia/Seoul", out var tz))
  92. {
  93. return tz;
  94. }
  95. if (TimeZoneInfo.TryFindSystemTimeZoneById("Korea Standard Time", out tz))
  96. {
  97. return tz;
  98. }
  99. // KST 는 DST 없음 — 고정 +9 fallback
  100. return TimeZoneInfo.CreateCustomTimeZone("KST", TimeSpan.FromHours(9), "KST", "KST");
  101. }
  102. }