## CRITICAL 수정 (20건) - day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가 - trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산 - risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수 - risk_management/recommendations: date.today() → KST 변환 - mdd_manager: Lock 밖 executed_key 참조 수정 - correlation_filter: output → output2 키 수정 - portfolio: 해외 종목 USD/KRW 환율 적용 분리 - Sharpe Ratio 거래빈도 기반 연간화 - telegram_notifier: send_message() 공개 래퍼 추가 - position_manager: 앱 재시작 시 DB 포지션 복원 - swing exit_service: remaining_pct 조건 범위 수정 - run_backtest: original_qty 기반 remaining_pct 계산 - us_market_schedule: 2027년 미국 공휴일 추가 - Login.tsx: res.ok 검증 추가 - mean_reversion_strategy: RSI 로그 하드코딩 수정 ## HIGH 수정 (10건) - db_worker: DLQ 파일 백업 메커니즘 추가 - korea_investment_api: 만료 토큰 임의 연장 제거 - market_data: chart_cache 최대 100개 + TTL 만료 - main.py: rate_limit_store 1시간 미접근 IP 정리 - recommendations: fire-and-forget try/except 추가 - swing/scheduler: DB 세션 3개 분리 - Dashboard: wsMessages 전체 순회 - Layout: /health 엔드포인트 + 30초 폴링 - ml_ensemble: POST /train require_api_key 추가 - day_trading: record_trade_result_safe() 전환 ## 코드 정리 - 프론트엔드: isUsStock, formatTime 공통 유틸 추출 - 백엔드: dead code 제거, 중복 import 통합 - MCP: JSON 파싱 헬퍼 추출 (~60줄 감소) - 전체 ~100개 파일 한국어 주석 추가 - 테스트 13건 코드 기준 수정 (468 passed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
274 lines
8.6 KiB
Python
274 lines
8.6 KiB
Python
"""미국 시장 스케줄 관리
|
|
|
|
- 미국 정규장: 09:30~16:00 ET (한국시간 23:30~06:00, 서머타임 시 22:30~05:00)
|
|
- 프리마켓: 04:00~09:30 ET
|
|
- 애프터마켓: 16:00~20:00 ET
|
|
- 미국 공휴일 관리
|
|
"""
|
|
import logging
|
|
from datetime import datetime, time, timedelta
|
|
from typing import Optional
|
|
|
|
import pytz
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── 타임존 ────────────────────────────────────────────────────────────────────
|
|
ET = pytz.timezone("US/Eastern")
|
|
KST = pytz.timezone("Asia/Seoul")
|
|
|
|
# ── 미국 시장 시간 (Eastern Time) ─────────────────────────────────────────────
|
|
PREMARKET_OPEN = time(4, 0) # 04:00 ET
|
|
MARKET_OPEN = time(9, 30) # 09:30 ET
|
|
MARKET_CLOSE = time(16, 0) # 16:00 ET
|
|
AFTERMARKET_CLOSE = time(20, 0) # 20:00 ET
|
|
|
|
# ── 2026년 미국 주식시장 휴장일 ──────────────────────────────────────────────
|
|
# NYSE/NASDAQ 공식 휴장일 (반장 제외)
|
|
US_HOLIDAYS_2026 = {
|
|
# New Year's Day
|
|
datetime(2026, 1, 1).date(),
|
|
# Martin Luther King Jr. Day (3rd Monday of January)
|
|
datetime(2026, 1, 19).date(),
|
|
# Presidents' Day (3rd Monday of February)
|
|
datetime(2026, 2, 16).date(),
|
|
# Good Friday
|
|
datetime(2026, 4, 3).date(),
|
|
# Memorial Day (last Monday of May)
|
|
datetime(2026, 5, 25).date(),
|
|
# Juneteenth National Independence Day
|
|
datetime(2026, 6, 19).date(),
|
|
# Independence Day (observed)
|
|
datetime(2026, 7, 3).date(),
|
|
# Labor Day (1st Monday of September)
|
|
datetime(2026, 9, 7).date(),
|
|
# Thanksgiving Day (4th Thursday of November)
|
|
datetime(2026, 11, 26).date(),
|
|
# Christmas Day
|
|
datetime(2026, 12, 25).date(),
|
|
}
|
|
|
|
# 추가 연도 지원을 위한 동적 휴일 계산
|
|
US_HOLIDAYS_2025 = {
|
|
datetime(2025, 1, 1).date(),
|
|
datetime(2025, 1, 20).date(),
|
|
datetime(2025, 2, 17).date(),
|
|
datetime(2025, 4, 18).date(),
|
|
datetime(2025, 5, 26).date(),
|
|
datetime(2025, 6, 19).date(),
|
|
datetime(2025, 7, 4).date(),
|
|
datetime(2025, 9, 1).date(),
|
|
datetime(2025, 11, 27).date(),
|
|
datetime(2025, 12, 25).date(),
|
|
}
|
|
|
|
# ── 2027년 미국 주식시장 휴장일 ──────────────────────────────────────────────
|
|
# NYSE/NASDAQ 공식 휴장일 (반장 제외)
|
|
US_HOLIDAYS_2027 = {
|
|
# New Year's Day (Friday)
|
|
datetime(2027, 1, 1).date(),
|
|
# Martin Luther King Jr. Day (3rd Monday of January)
|
|
datetime(2027, 1, 18).date(),
|
|
# Presidents' Day (3rd Monday of February)
|
|
datetime(2027, 2, 15).date(),
|
|
# Good Friday (Easter 2027-03-28 기준)
|
|
datetime(2027, 3, 26).date(),
|
|
# Memorial Day (last Monday of May)
|
|
datetime(2027, 5, 31).date(),
|
|
# Juneteenth (June 19 = Saturday → Friday June 18 observed)
|
|
datetime(2027, 6, 18).date(),
|
|
# Independence Day (July 4 = Sunday → Monday July 5 observed)
|
|
datetime(2027, 7, 5).date(),
|
|
# Labor Day (1st Monday of September)
|
|
datetime(2027, 9, 6).date(),
|
|
# Thanksgiving Day (4th Thursday of November)
|
|
datetime(2027, 11, 25).date(),
|
|
# Christmas Day (December 25 = Saturday → Friday December 24 observed)
|
|
datetime(2027, 12, 24).date(),
|
|
}
|
|
|
|
_ALL_HOLIDAYS = US_HOLIDAYS_2025 | US_HOLIDAYS_2026 | US_HOLIDAYS_2027
|
|
|
|
|
|
def _get_holidays_set():
|
|
"""holidays 패키지가 있으면 사용, 없으면 하드코딩 세트 반환."""
|
|
try:
|
|
import holidays as holidays_pkg
|
|
return holidays_pkg.US(years=[2025, 2026, 2027, 2028], categories={"public"})
|
|
except ImportError:
|
|
return None
|
|
|
|
|
|
def _now_et() -> datetime:
|
|
"""현재 시각을 US/Eastern 기준으로 반환 (timezone-aware)."""
|
|
return datetime.now(ET)
|
|
|
|
|
|
def is_dst() -> bool:
|
|
"""현재 미국이 서머타임(DST) 기간인지 확인.
|
|
|
|
DST 기간: 3월 둘째 일요일 ~ 11월 첫째 일요일
|
|
DST 기간에는 한국과의 시차가 13시간 (비DST: 14시간).
|
|
|
|
Returns:
|
|
True if 현재 DST 적용 중
|
|
"""
|
|
now = _now_et()
|
|
# pytz의 dst()는 DST 오프셋을 반환 (0이면 비DST)
|
|
return bool(now.dst())
|
|
|
|
|
|
def is_us_holiday(date: Optional[datetime] = None) -> bool:
|
|
"""해당 날짜가 미국 주식시장 휴장일인지 확인.
|
|
|
|
Args:
|
|
date: 확인할 날짜. None이면 현재 ET 날짜 사용.
|
|
datetime 또는 date 객체 모두 허용.
|
|
|
|
Returns:
|
|
True if 휴장일
|
|
"""
|
|
if date is None:
|
|
check_date = _now_et().date()
|
|
elif isinstance(date, datetime):
|
|
check_date = date.date()
|
|
else:
|
|
check_date = date
|
|
|
|
# holidays 패키지 시도
|
|
holidays_cal = _get_holidays_set()
|
|
if holidays_cal is not None:
|
|
return check_date in holidays_cal
|
|
|
|
# 하드코딩 fallback
|
|
return check_date in _ALL_HOLIDAYS
|
|
|
|
|
|
def is_us_market_open() -> bool:
|
|
"""미국 정규장이 현재 열려있는지 확인.
|
|
|
|
조건:
|
|
- 평일 (월~금)
|
|
- 휴장일 아님
|
|
- 09:30 ET <= 현재시각 < 16:00 ET
|
|
|
|
Returns:
|
|
True if 정규 거래시간
|
|
"""
|
|
now = _now_et()
|
|
|
|
# 주말 체크 (토: 5, 일: 6)
|
|
if now.weekday() >= 5:
|
|
return False
|
|
|
|
# 휴장일 체크
|
|
if is_us_holiday(now):
|
|
return False
|
|
|
|
current_time = now.time()
|
|
return MARKET_OPEN <= current_time < MARKET_CLOSE
|
|
|
|
|
|
def is_us_premarket() -> bool:
|
|
"""미국 프리마켓 시간인지 확인.
|
|
|
|
프리마켓: 04:00 ~ 09:30 ET (평일, 비휴장일)
|
|
|
|
Returns:
|
|
True if 프리마켓 시간
|
|
"""
|
|
now = _now_et()
|
|
|
|
if now.weekday() >= 5:
|
|
return False
|
|
|
|
if is_us_holiday(now):
|
|
return False
|
|
|
|
current_time = now.time()
|
|
return PREMARKET_OPEN <= current_time < MARKET_OPEN
|
|
|
|
|
|
def is_us_aftermarket() -> bool:
|
|
"""미국 애프터마켓 시간인지 확인.
|
|
|
|
애프터마켓: 16:00 ~ 20:00 ET (평일, 비휴장일)
|
|
|
|
Returns:
|
|
True if 애프터마켓 시간
|
|
"""
|
|
now = _now_et()
|
|
|
|
if now.weekday() >= 5:
|
|
return False
|
|
|
|
if is_us_holiday(now):
|
|
return False
|
|
|
|
current_time = now.time()
|
|
return MARKET_CLOSE <= current_time < AFTERMARKET_CLOSE
|
|
|
|
|
|
def get_next_us_market_open() -> datetime:
|
|
"""다음 미국 정규장 개장 시각 반환 (timezone-aware, US/Eastern).
|
|
|
|
현재 정규장이 열려있으면 오늘 개장 시각을 반환.
|
|
그렇지 않으면 다음 거래일의 09:30 ET을 반환.
|
|
|
|
Returns:
|
|
다음 개장 시각 (datetime, US/Eastern timezone)
|
|
"""
|
|
now = _now_et()
|
|
today_open = now.replace(hour=9, minute=30, second=0, microsecond=0)
|
|
|
|
# 오늘이 거래일이고 아직 개장 전이면 → 오늘 09:30
|
|
if now.weekday() < 5 and not is_us_holiday(now) and now.time() < MARKET_OPEN:
|
|
return ET.localize(today_open.replace(tzinfo=None)) if today_open.tzinfo is None else today_open
|
|
|
|
# 오늘이 거래일이고 정규장 중이면 → 오늘 09:30
|
|
if is_us_market_open():
|
|
return ET.localize(today_open.replace(tzinfo=None)) if today_open.tzinfo is None else today_open
|
|
|
|
# 다음 거래일 탐색 (최대 10일 — 연휴 대비)
|
|
next_day = now + timedelta(days=1)
|
|
for _ in range(10):
|
|
next_day = next_day.replace(hour=9, minute=30, second=0, microsecond=0)
|
|
if next_day.weekday() < 5 and not is_us_holiday(next_day):
|
|
if next_day.tzinfo is None:
|
|
return ET.localize(next_day)
|
|
return next_day
|
|
next_day += timedelta(days=1)
|
|
|
|
# fallback: 내일 09:30 (이론적으로 도달 불가)
|
|
fallback = (now + timedelta(days=1)).replace(hour=9, minute=30, second=0, microsecond=0)
|
|
return ET.localize(fallback.replace(tzinfo=None)) if fallback.tzinfo is None else fallback
|
|
|
|
|
|
def get_us_market_status() -> dict:
|
|
"""현재 미국 시장 상태 요약 반환.
|
|
|
|
Returns:
|
|
dict with keys:
|
|
- status: 'OPEN', 'PREMARKET', 'AFTERMARKET', 'CLOSED'
|
|
- is_dst: bool
|
|
- current_et: str (현재 ET 시각)
|
|
- next_open: str (다음 개장 시각)
|
|
"""
|
|
now = _now_et()
|
|
|
|
if is_us_market_open():
|
|
status = "OPEN"
|
|
elif is_us_premarket():
|
|
status = "PREMARKET"
|
|
elif is_us_aftermarket():
|
|
status = "AFTERMARKET"
|
|
else:
|
|
status = "CLOSED"
|
|
|
|
return {
|
|
"status": status,
|
|
"is_dst": is_dst(),
|
|
"current_et": now.strftime("%Y-%m-%d %H:%M:%S %Z"),
|
|
"next_open": get_next_us_market_open().strftime("%Y-%m-%d %H:%M:%S %Z"),
|
|
}
|