Files
kis_bot/kis_trader/utils/kr_trading_day.py
Your Name fc27e726f9 feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes):

구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함.

스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함.

코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함.

시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함.

기대 효과 (Impact):

이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
2026-07-17 01:09:09 +09:00

233 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
kis_trader/utils/kr_trading_day.py — 한국 거래일(주말·휴장) 보정 공통
====================================================================
백테 웹 / CLI / API 날짜 기본값: 주말·휴장이면 **이전 장운영일**로 맞춤.
휴장일 목록: env ``KR_MARKET_HOLIDAYS`` (콤마구분 YYYY-MM-DD).
DB에 없으면 기본 CSV를 env_config_ext 에 1회 시드.
"""
from __future__ import annotations
from datetime import date, datetime, timedelta
from typing import List, Optional, Set, Tuple, Union
from kis_trader.utils.env import get_env_from_db, get_env_int
DateLike = Union[str, date, datetime]
# 한국거래소 휴장(주말 제외). DB/ env 로 갱신. get_env 기본값용.
_DEFAULT_KR_MARKET_HOLIDAYS = (
# 2025
"2025-01-01,2025-01-28,2025-01-29,2025-01-30,"
"2025-03-01,2025-05-05,2025-05-06,2025-06-06,"
"2025-08-15,2025-10-03,2025-10-06,2025-10-07,2025-10-08,2025-10-09,2025-12-25,"
# 2026
"2026-01-01,2026-02-16,2026-02-17,2026-02-18,"
"2026-03-01,2026-05-05,2026-05-24,2026-06-06,"
"2026-08-15,2026-09-24,2026-09-25,2026-09-26,"
"2026-10-03,2026-10-09,2026-12-25,"
# 2027 (주요)
"2027-01-01,2027-02-08,2027-02-09,2027-02-10,"
"2027-03-01,2027-05-05,2027-05-13,2027-06-06,"
"2027-08-15,2027-09-14,2027-09-15,2027-09-16,"
"2027-10-03,2027-10-09,2027-12-25"
)
_holidays_cache: Optional[Set[date]] = None
_holidays_cache_raw: Optional[str] = None
_seeded: bool = False
def _parse_ymd(raw: DateLike) -> date:
if isinstance(raw, datetime):
return raw.date()
if isinstance(raw, date):
return raw
s = str(raw or "").strip()
if not s:
raise ValueError("empty date")
if "-" in s and len(s) >= 10:
return datetime.strptime(s[:10], "%Y-%m-%d").date()
digits = "".join(c for c in s if c.isdigit())
if len(digits) >= 8:
return datetime.strptime(digits[:8], "%Y%m%d").date()
raise ValueError(f"bad date: {raw!r}")
def ymd(d: date) -> str:
return d.strftime("%Y-%m-%d")
def parse_holiday_csv(raw: str) -> Set[date]:
out: Set[date] = set()
for part in str(raw or "").replace(";", ",").split(","):
p = part.strip()
if not p or p.startswith("#"):
continue
try:
out.add(_parse_ymd(p))
except ValueError:
continue
return out
def ensure_kr_market_holidays_in_db() -> None:
"""KR_MARKET_HOLIDAYS / lookback 기본값을 DB/ext 에 없으면 1회 시드 (기존 값 유지)."""
global _seeded
if _seeded:
return
_seeded = True
try:
from kis_trader.utils.env import _get_db # noqa: PLC2701
db = _get_db()
if not db:
return
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def _seed_if_missing(key: str, value: str) -> None:
cur = str(get_env_from_db(key, "") or "").strip()
if cur:
return
row = db.conn.execute(
"SELECT env_value FROM env_config_ext WHERE env_key=%s LIMIT 1",
(key,),
).fetchone()
if row and str((row["env_value"] if isinstance(row, dict) else row[0]) or "").strip():
return
db.conn.execute(
"INSERT INTO env_config_ext (env_key, env_value, updated_at) "
"VALUES (%s, %s, %s) "
"ON DUPLICATE KEY UPDATE env_key=env_key",
(key, value, now),
)
_seed_if_missing("KR_MARKET_HOLIDAYS", _DEFAULT_KR_MARKET_HOLIDAYS)
_seed_if_missing("PARAM_SEARCH_DEFAULT_LOOKBACK_DAYS", "7")
_seed_if_missing("KR_TRADING_DAY_MAX_BACK_DAYS", "14")
try:
from kis_trader.utils.env import invalidate_merged_env_cache
invalidate_merged_env_cache()
except Exception:
pass
except Exception:
pass
def get_kr_market_holiday_set(*, refresh: bool = False) -> Set[date]:
"""휴장일 set. env/DB → 없으면 코드 기본 CSV."""
global _holidays_cache, _holidays_cache_raw
ensure_kr_market_holidays_in_db()
raw = str(
get_env_from_db("KR_MARKET_HOLIDAYS", _DEFAULT_KR_MARKET_HOLIDAYS) or ""
).strip()
if not raw:
raw = _DEFAULT_KR_MARKET_HOLIDAYS
if (
not refresh
and _holidays_cache is not None
and _holidays_cache_raw == raw
):
return _holidays_cache
_holidays_cache = parse_holiday_csv(raw)
_holidays_cache_raw = raw
return _holidays_cache
def is_kr_trading_day(d: DateLike, *, holidays: Optional[Set[date]] = None) -> bool:
"""월~금 이고 휴장 목록에 없으면 True."""
day = _parse_ymd(d)
if day.weekday() >= 5:
return False
hol = holidays if holidays is not None else get_kr_market_holiday_set()
return day not in hol
def clamp_to_prev_kr_trading_day(
d: DateLike,
*,
holidays: Optional[Set[date]] = None,
max_back_days: Optional[int] = None,
) -> str:
"""
거래일이 아니면 하루씩 과거로 이동해 **가장 가까운 이전(또는 당일) 장운영일** 반환.
"""
day = _parse_ymd(d)
hol = holidays if holidays is not None else get_kr_market_holiday_set()
limit = int(
max_back_days
if max_back_days is not None
else get_env_int("KR_TRADING_DAY_MAX_BACK_DAYS", 14)
)
limit = max(1, limit)
for _ in range(limit + 1):
if is_kr_trading_day(day, holidays=hol):
return ymd(day)
day -= timedelta(days=1)
return ymd(day)
def default_kr_trading_end(*, as_of: Optional[DateLike] = None) -> str:
"""종료일 기본값 = as_of(기본 오늘) 을 이전 거래일로 보정."""
base = _parse_ymd(as_of) if as_of is not None else date.today()
return clamp_to_prev_kr_trading_day(base)
def default_kr_trading_start(
lookback_days: int = 7,
*,
as_of: Optional[DateLike] = None,
end: Optional[DateLike] = None,
) -> str:
"""시작일 기본값 = (종료일 lookback) 을 이전 거래일로 보정."""
end_d = _parse_ymd(end) if end is not None else _parse_ymd(
default_kr_trading_end(as_of=as_of)
)
lb = max(1, int(lookback_days))
start_raw = end_d - timedelta(days=lb)
return clamp_to_prev_kr_trading_day(start_raw)
def resolve_kr_backtest_date_defaults(
lookback_days: int = 7,
*,
as_of: Optional[DateLike] = None,
) -> Tuple[str, str]:
"""(start, end) 기본 구간. end·start 모두 거래일."""
end = default_kr_trading_end(as_of=as_of)
start = default_kr_trading_start(lookback_days, end=end)
if start > end:
start = end
return start, end
def clamp_to_prev_weekday(d: DateLike, *, max_back_days: int = 14) -> str:
"""주말만 보정 (해외장 등 공휴일 목록 없이 토·일만 스킵)."""
day = _parse_ymd(d)
for _ in range(max(1, int(max_back_days)) + 1):
if day.weekday() < 5:
return ymd(day)
day -= timedelta(days=1)
return ymd(day)
def holiday_list_ymd() -> List[str]:
return sorted(ymd(d) for d in get_kr_market_holiday_set())
def trading_dates_payload(lookback_days: int = 7) -> dict:
"""웹/API용 날짜 기본값 묶음."""
start, end = resolve_kr_backtest_date_defaults(lookback_days)
today = ymd(date.today())
return {
"today": today,
"end": end,
"start": start,
"trading_day": end, # 단일일 조회용 (= 보정된 종료일)
"holidays": holiday_list_ymd(),
"lookback_days": int(lookback_days),
"is_today_trading_day": is_kr_trading_day(today),
}