Coverage for core / market_clock.py: 100%
80 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
1# core/market_clock.py
2import time
3from typing import Optional
4import pytz
5import logging
6import asyncio
7from datetime import datetime, timedelta, date, time as dt_time
9class MarketClock:
10 """
11 주식 거래와 관련된 '시간(시계)'을 관리하는 클래스입니다.
12 순수 시간대 계산 및 포맷 변환, KST 타임존 처리를 담당하며,
13 공휴일 및 실제 영업일 판단은 MarketCalendarService에서 수행해야 합니다.
14 """
16 def __init__(self, market_open_time="09:00", market_close_time="15:40", timezone="Asia/Seoul", logger=None):
17 self.market_open_time_str = market_open_time
18 self.market_close_time_str = market_close_time
19 self.timezone_name = timezone
20 self.logger = logger if logger else logging.getLogger(__name__)
22 # [최적화 1] 시간 문자열을 한 번만 파싱하여 time 객체로 캐싱
23 open_h, open_m = map(int, self.market_open_time_str.split(':'))
24 self._open_time_obj = dt_time(open_h, open_m)
25 close_h, close_m = map(int, self.market_close_time_str.split(':'))
26 self._close_time_obj = dt_time(close_h, close_m)
28 try:
29 self.market_timezone = pytz.timezone(self.timezone_name)
30 except pytz.UnknownTimeZoneError:
31 self.logger.error(f"알 수 없는 시간대: {self.timezone_name}. 'Asia/Seoul'로 기본 설정합니다.")
32 self.timezone_name = "Asia/Seoul"
33 self.market_timezone = pytz.timezone(self.timezone_name)
35 def get_current_kst_time(self):
36 """현재 한국 시간(KST)을 timezone-aware datetime 객체로 반환합니다."""
37 return datetime.now(self.market_timezone)
39 def get_current_kst_date_str(self):
40 """현재 KST 기준 날짜를 YYYYMMDD 포맷으로 반환합니다."""
41 return self.get_current_kst_time().strftime("%Y%m%d")
43 def is_market_operating_hours(self, now=None) -> bool:
44 """
45 단순히 현재 '시간'이 시장 운영 시간(예: 09:00 ~ 15:40) 내에 있는지 확인합니다.
46 (주의: 공휴일, 임시휴일 등 '영업일' 여부는 MarketCalendarService에서 판단해야 합니다.)
47 """
48 now = now or self.get_current_kst_time()
50 # 주말(토, 일)은 기본적으로 1차 제외
51 if now.weekday() >= 5:
52 return False
54 # [최적화 2] 무거운 datetime 조합과 타임존 연산 없이 순수 시간(time) 객체만으로 비교
55 return self._open_time_obj <= now.time() <= self._close_time_obj
57 def get_market_open_time(self, target_dt: Optional[datetime] = None) -> datetime:
58 """오늘 날짜 또는 지정된 날짜 기준 시장 개장 시간(09:00) 반환"""
59 now = target_dt or self.get_current_kst_time()
60 return self.market_timezone.localize(datetime(
61 now.year, now.month, now.day,
62 hour=self._open_time_obj.hour,
63 minute=self._open_time_obj.minute,
64 second=0, microsecond=0
65 ))
67 def get_market_close_time(self, target_dt: Optional[datetime] = None) -> datetime:
68 """오늘 날짜 또는 지정된 날짜 기준 시장 마감 시간(15:40) 반환"""
69 now = target_dt or self.get_current_kst_time()
70 return self.market_timezone.localize(datetime(
71 now.year, now.month, now.day,
72 hour=self._close_time_obj.hour,
73 minute=self._close_time_obj.minute,
74 second=0, microsecond=0
75 ))
77 def get_seconds_until_market_close(self, now=None) -> float:
78 """
79 현재 시간 또는 지정된 시간부터 해당 날짜의 장 마감(15:40)까지 남은 초(seconds)를 계산합니다.
80 (장 마감 후 계산 시 음수가 반환될 수 있습니다.)
81 """
82 now = now or self.get_current_kst_time()
83 close_time = self.get_market_close_time(target_dt=now)
84 diff = (close_time - now).total_seconds()
85 return diff
87 def get_sleep_seconds_until_market_close(self, now=None) -> float:
88 """
89 현재 시간부터 오늘 장 마감(15:40)까지 대기해야 할 남은 초를 반환합니다.
90 이미 마감 시간을 지났다면 0.0을 반환합니다.
91 """
92 diff = self.get_seconds_until_market_close(now)
93 return max(0.0, diff)
95 def sleep(self, seconds):
96 """지정된 시간(초)만큼 프로그램을 일시 중지합니다 (동기)."""
97 if seconds > 0:
98 self.logger.info(f"{seconds:.2f}초 동안 대기합니다 (동기).")
99 time.sleep(seconds)
101 async def async_sleep(self, seconds):
102 """지정된 시간(초)만큼 비동기적으로 프로그램을 일시 중지합니다."""
103 if seconds > 0:
104 self.logger.info(f"{seconds:.2f}초 동안 대기합니다 (비동기).")
105 await asyncio.sleep(seconds)
107 def to_yyyymmdd(self, val) -> str:
108 """여러 타입을 YYYYMMDD 문자열로 안전 변환"""
109 if val is None:
110 return self.get_current_kst_date_str()
111 if isinstance(val, str):
112 return val
113 if isinstance(val, (datetime, date)):
114 return val.strftime("%Y%m%d")
115 if callable(val):
116 return self.to_yyyymmdd(val())
117 return str(val)
119 def to_hhmmss(self, t: str | int) -> str:
120 """
121 다양한 입력(YYYYMMDDHH, YYYYMMDDHHMM, HH, HHMM 등)을 안전하게 HHMMSS로 정규화.
122 규칙:
123 - 긴 포맷은 뒤 6자리만 취함
124 - HH만 오면 HH0000, HHMM이면 HHMM00
125 - 애매한 길이(1/3/5자)는 왼쪽 0 패딩
126 """
127 if t is None:
128 t = self.get_current_kst_time()
130 s = ''.join(ch for ch in str(t).strip() if ch.isdigit())
132 if len(s) == 2: # HH
133 return s + "0000"
134 if len(s) == 4: # HHMM
135 return s + "00"
137 if len(s) >= 6:
138 return s[-6:]
139 return s.rjust(6, "0")
141 def dec_minute(self, hhmmss: str, minutes: int = 1) -> str:
142 """HHMMSS 포맷의 문자열 시간에서 특정 분(minute)을 뺀 시간을 반환합니다."""
143 hh = int(hhmmss[0:2])
144 mm = int(hhmmss[2:4])
145 ss = int(hhmmss[4:6])
146 dt = datetime(2000, 1, 1, hh, mm, ss) - timedelta(minutes=minutes)
147 return dt.strftime("%H%M%S")