AlgoLab Blog · 봇 운영 실무 · 2026

주식 휴장일 조회 — 봇 개장일 판정 3가지 비교

운영 · 스케줄링 2026-08-09 · 약 9분 읽기 · 알고랩 AlgoLab
한 줄 요약 자동매매 봇의 개장일 판정은 세 가지 중 하나로 하고, 실거래 봇이라면 증권사 API를 최종 기준으로 씁니다.한국투자증권 KIS API 국내휴장일조회 — 경로 /uapi/domestic-stock/v1/quotations/chk-holiday, tr_idCTCA0903R, 기준일자 BASS_DT를 넣고 응답의 opnd_yn(개장일여부)을 본다. ② pykrxstock.get_previous_business_days()로 실제 거래가 있었던 날을 확인(과거·백테스트용). ③ exchange_calendarsXKRX 캘린더의 is_session()으로 미래 날짜까지 즉시 판정.
⚠️ 공식 예제는 CTCA0903R가급적 하루 한 번만 호출하라고 안내하므로 반드시 캐시해야 하고, 서버 시간대는 Asia/Seoul명시해야 날짜가 하루 밀리지 않습니다.

봇을 처음 실서버에 올리고 나면 대체로 이런 일이 벌어집니다. 공휴일 아침에 텔레그램 알림이 수십 통 쏟아지는 겁니다. 봇은 평소처럼 09시에 깨어나 시세를 조회하고, 신호가 나오자 주문을 던지고, 거부당하고, 재시도하고, 또 거부당합니다. 코드에는 아무 버그가 없습니다. 그저 "오늘 시장이 열리는가"를 아무도 확인하지 않았을 뿐입니다.

KIS API 키를 발급받고 첫 주문까지 마쳤다면, 실서버로 넘어가기 직전에 반드시 붙여야 하는 장치입니다.

목차

  1. 주말만 걸러서는 왜 부족한가
  2. 방법 ① KIS 국내휴장일조회 — CTCA0903R
  3. 방법 ② pykrx — 실제 거래가 있었던 날
  4. 방법 ③ exchange_calendarsXKRX 캘린더
  5. 셋 중 뭘 쓰나 — 비교표
  6. 봇에 붙이는 구조 — 하루 한 번 캐시 + 스케줄러
  7. 실제로 사고가 나는 함정 4가지

1. 주말만 걸러서는 왜 부족한가

많은 초기 코드가 이렇게 생겼습니다.

if datetime.now().weekday() >= 5:      # 5=토, 6=일
    print("주말입니다. 오늘은 쉽니다.")
    sys.exit(0)

이 코드는 1년에 열몇 번 틀립니다. 한국 증시가 문을 닫는 날은 주말 말고도 이만큼 있습니다.

특히 대체공휴일과 임시공휴일이 문제입니다. 이 둘은 상수로 박아둘 수 없습니다. 그래서 봇은 매일 아침 "오늘 열리나"를 물어봐야 합니다.

휴장일에 주문을 던지면 뭐가 나쁜가. 대부분의 증권사 REST API는 장운영시간 밖 주문을 오류로 거부합니다. 문제는 거부 자체가 아니라 그 뒤입니다 — 봇의 재시도 로직이 돌기 시작하고, 호출 제한에 걸려 EGW00201 같은 에러가 연쇄로 뜨고, 알림이 폭주해 정작 봐야 할 진짜 장애 알림이 묻힙니다. 개장일이 아니면 전략 루프에 아예 진입하지 않는 것이 정답입니다.

08:40 봇 기동 Asia/Seoul 개장일 판정 CTCA0903R opnd_yn Y N 캐시 저장 후 전략 루프 진입 알림 1건만 보내고 즉시 종료 시간대 확인 09:00~15:30 1일 1회 조회 · 결과는 파일/메모리 캐시
개장일 판정은 전략 루프 바깥에, 하루 한 번만

2. 방법 ① KIS 국내휴장일조회 — CTCA0903R

한국투자증권 KIS Developers는 국내휴장일조회를 별도 API로 제공합니다. 공식 깃허브 저장소 open-trading-api의 예제 코드 기준으로 스펙은 이렇습니다.

항목
경로/uapi/domestic-stock/v1/quotations/chk-holiday
tr_idCTCA0903R
메서드GET
필수 파라미터BASS_DT (기준일자, YYYYMMDD)
연속조회CTX_AREA_FK · CTX_AREA_NK
핵심 응답 필드opnd_yn (개장일여부) — 주문 가능 판단은 이것
호출 정책가급적 1일 1회 (공식 예제 주석의 안내)

왜 1일 1회인가. 공식 예제 주석에는 이 서비스가 원장 서비스와 연관되어 있어 단시간 내 다수 호출 시 서비스에 영향을 줄 수 있다는 취지의 안내가 붙어 있습니다. 초당 호출 제한(EGW00201)과는 성격이 다른 운영상 요청입니다. 결론은 같습니다 — 루프 안에서 부르지 말고 캐시하십시오.

실제 호출 코드

access_tokenappkey·appsecret은 이미 준비돼 있다고 가정합니다(토큰 수명 관리는 KIS API 토큰 만료·재발급 글에서 따로 다룹니다).

import requests
from datetime import datetime
from zoneinfo import ZoneInfo

BASE = "https://openapi.koreainvestment.com:9443"   # 실전
# BASE = "https://openapivts.koreainvestment.com:29443"  # 모의

def fetch_holiday_calendar(access_token, appkey, appsecret, bass_dt=None):
    if bass_dt is None:
        # ★ 시간대를 반드시 명시 — 서버가 UTC면 하루 밀린다
        bass_dt = datetime.now(ZoneInfo("Asia/Seoul")).strftime("%Y%m%d")

    url = BASE + "/uapi/domestic-stock/v1/quotations/chk-holiday"
    headers = {
        "content-type": "application/json; charset=utf-8",
        "authorization": f"Bearer {access_token}",
        "appkey": appkey,
        "appsecret": appsecret,
        "tr_id": "CTCA0903R",
        "custtype": "P",
    }
    params = {
        "BASS_DT": bass_dt,
        "CTX_AREA_FK": "",
        "CTX_AREA_NK": "",
    }
    res = requests.get(url, headers=headers, params=params, timeout=10)
    res.raise_for_status()
    return res.json()

응답은 이렇게 생겼습니다

응답의 output기준일자부터의 날짜 배열이고, 날짜마다 네 가지 여부 플래그가 따로 들어옵니다. 아래는 필드 구조를 보여주기 위한 형태 예시입니다.

{
  "rt_cd": "0",
  "msg_cd": "MCA00000",
  "msg1": "정상처리 되었습니다.",
  "ctx_area_fk": "...",
  "ctx_area_nk": "...",
  "output": [
    { "bass_dt": "20260809", "wday_dvsn_cd": "01",
      "bzdy_yn": "N", "tr_day_yn": "N", "opnd_yn": "N", "sttl_day_yn": "N" },
    { "bass_dt": "20260810", "wday_dvsn_cd": "02",
      "bzdy_yn": "Y", "tr_day_yn": "Y", "opnd_yn": "Y", "sttl_day_yn": "Y" }
  ]
}

네 플래그의 뜻이 헷갈리는데, 봇이 봐야 할 것은 opnd_yn 하나입니다. 공식 예제 주석이 "주문을 넣을 수 있는지 확인하려면 개장일여부(opnd_yn)를 사용하라"고 명시하고 있습니다.

필드봇에서
opnd_yn개장일 여부이걸 씁니다
tr_day_yn거래일 여부참고
bzdy_yn영업일 여부증권사 영업 기준
sttl_day_yn결제일 여부정산·예수금 계산용

오늘이 개장일인지만 뽑는 함수

def is_open_today(access_token, appkey, appsecret):
    today = datetime.now(ZoneInfo("Asia/Seoul")).strftime("%Y%m%d")
    data = fetch_holiday_calendar(access_token, appkey, appsecret, today)

    if data.get("rt_cd") != "0":
        # 조회 자체가 실패하면 '열렸다'고 가정하지 말 것
        raise RuntimeError(f"휴장일 조회 실패: {data.get('msg_cd')} {data.get('msg1')}")

    for row in data.get("output", []):
        if row.get("bass_dt") == today:
            return row.get("opnd_yn") == "Y"
    raise RuntimeError("응답에 오늘 날짜가 없습니다")

실패 시 기본값을 True로 두지 마십시오. try/except로 감싸고 예외가 나면 "일단 열린 걸로 치자"고 넘어가는 코드가 흔한데, 이러면 네트워크가 잠깐 흔들린 공휴일 아침에 정확히 사고가 납니다. 조회에 실패하면 주문을 만들지 않고 알림 한 통을 보낸 뒤 종료하는 쪽이 안전합니다. 봇의 기본값은 언제나 "아무것도 하지 않음"이어야 합니다.

💬
"이런 것까지 다 만들어야 하나요?"

휴장일 판정, 시간대 처리, 실패 시 정지 — 전략보다 이런 운영 배관이 개발 시간의 절반을 먹습니다. 알고랩은 전략을 받아 이 배관까지 포함해 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기 →

3. 방법 ② pykrx — 실제 거래가 있었던 날

pykrx는 한국거래소 데이터를 가져오는 파이썬 라이브러리입니다. 휴장일 전용 함수가 있는 건 아니고, "실제로 거래가 체결된 날"을 되짚는 방식으로 씁니다.

from pykrx import stock

# 해당 월의 영업일 목록
days = stock.get_previous_business_days(year=2026, month=8)
print(days[:3])
# [Timestamp('2026-08-03 00:00:00'), Timestamp('2026-08-04 00:00:00'), ...]

target = "20260810"
opened = any(d.strftime("%Y%m%d") == target for d in days)
print(opened)

장점은 인증이 필요 없다는 것입니다. appkeyaccess_token도 없이 바로 됩니다. 그래서 백테스트에서 거래일 인덱스를 만들 때 특히 편합니다.

단점은 미래를 모른다는 것입니다. 이 함수는 이미 지나간 거래 기록을 근거로 하므로, 내일이 개장일인지 묻는 용도로는 맞지 않습니다. 또 외부 사이트의 응답 형식에 의존하므로 그쪽이 바뀌면 함께 멈출 수 있습니다. pykrxFinanceDataReader의 성격 차이는 파이썬 주식 데이터 수집 비교에서 더 자세히 다뤘습니다.

4. 방법 ③ exchange_calendarsXKRX 캘린더

exchange_calendars는 전 세계 거래소의 개장 달력을 담은 라이브러리이고, 한국거래소는 XKRX라는 코드로 들어 있습니다.

import exchange_calendars as xcals

krx = xcals.get_calendar("XKRX")

print(krx.is_session("2026-08-10"))     # True  → 개장일
print(krx.is_session("2026-08-15"))     # False → 광복절

# 구간 내 개장일 전체
sessions = krx.sessions_in_range("2026-08-01", "2026-08-31")
print(len(sessions))

가장 큰 장점은 미래 날짜도 즉시 판정된다는 점입니다. "다음 리밸런싱은 언제인가", "이번 달 거래일이 며칠인가" 같은 계산이 한 줄로 끝납니다. 인증도 필요 없습니다.

대신 임시공휴일에 취약합니다. 이 라이브러리는 내장된 휴일 규칙으로 달력을 만들기 때문에, 연중에 새로 지정된 임시공휴일은 라이브러리 버전이 갱신되기 전까지 반영되지 않을 수 있습니다. 실거래 봇에서 이걸 단독 기준으로 쓰면 바로 그 새로 생긴 휴일에 주문을 던지게 됩니다.

5. 셋 중 뭘 쓰나 — 비교표

KIS CTCA0903Rpykrxexchange_calendars
인증필요 (access_token)불필요불필요
미래 날짜가능불가가능
임시공휴일 반영가장 빠름사후 반영버전 갱신 의존
호출 제약1일 1회 권고외부 사이트 부하 주의로컬 계산, 없음
주 용도실거래 봇백테스트 거래일 인덱스일정 계산·미래 스케줄

권장 조합. 실거래 봇은 주문을 받을 증권사가 알려주는 개장일을 최종 기준으로 삼습니다(CTCA0903Ropnd_yn). exchange_calendars"다음 개장일이 언제인가" 같은 일정 계산에, pykrx백테스트 거래일 인덱스에 씁니다. 셋을 경쟁 관계로 볼 게 아니라 역할을 나누는 것이 맞습니다. 키움 REST API·LS증권 등 다른 증권사를 쓴다면 각사가 제공하는 동등한 조회 기능이 있는지 먼저 확인하십시오 — 국내 증권사 API 비교에 각사 특성을 정리해 뒀습니다.

6. 봇에 붙이는 구조 — 하루 한 번 캐시 + 스케줄러

CTCA0903R은 하루 한 번만 부르는 것이 권고이므로, 결과를 파일에 저장하고 날짜가 바뀔 때만 다시 부르는 구조를 씁니다. access_token 캐싱과 완전히 같은 패턴입니다.

import json, os
from datetime import datetime
from zoneinfo import ZoneInfo

CACHE = "market_open_cache.json"

def is_open_today_cached(access_token, appkey, appsecret):
    today = datetime.now(ZoneInfo("Asia/Seoul")).strftime("%Y%m%d")

    if os.path.exists(CACHE):
        with open(CACHE, encoding="utf-8") as f:
            c = json.load(f)
        if c.get("date") == today:          # 오늘 이미 물어봤다
            return c["open"]

    opened = is_open_today(access_token, appkey, appsecret)
    with open(CACHE, "w", encoding="utf-8") as f:
        json.dump({"date": today, "open": opened}, f)
    return opened

스케줄러에도 시간대를 넘긴다

APScheduler를 쓴다면 CronTrigger시간대를 명시해야 합니다. 이걸 빼면 UTC 서버에서 한국시간 18시에 봇이 깨어납니다.

from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger

sched = BlockingScheduler(timezone="Asia/Seoul")

@sched.scheduled_job(CronTrigger(day_of_week="mon-fri", hour=8, minute=40,
                                 timezone="Asia/Seoul"))
def premarket():
    if not is_open_today_cached(TOKEN, APPKEY, APPSECRET):
        notify("오늘은 휴장일입니다. 전략을 실행하지 않습니다.")
        return
    warm_up_universe()      # 종목 목록·잔고 로드
    start_strategy_loop()

sched.start()

day_of_week="mon-fri"로 주말은 스케줄러가 먼저 걸러 주고, 공휴일은 API가 걸러 줍니다. 두 겹입니다. 이런 상시 구동 구조를 서버에 올리는 방법은 VPS 24시간 봇 운영 가이드에 정리돼 있습니다.

7. 실제로 사고가 나는 함정 4가지

① 서버 시간대가 UTC라 날짜가 하루 밀린다

가장 흔합니다. 해외 리전 VPS나 도커 컨테이너는 기본이 UTC이고 한국시간과 9시간 차이입니다. 한국 기준 오전 9시가 UTC로는 전날 0시입니다. 그 상태로 datetime.now()를 쓰면 BASS_DT부터 어긋납니다. 서버 설정을 바꾸는 것보다 코드에서 ZoneInfo("Asia/Seoul")을 명시하는 편이 안전합니다. 다른 프로그램에 영향을 주지 않기 때문입니다.

② 개장일이어도 주문 가능 시간이 아닐 수 있다

opnd_yn == "Y"그날 시장이 열린다는 뜻이지 지금 주문이 된다는 뜻이 아닙니다. 장 시작 전 동시호가 구간과 장 종료 후 시간외 구간은 주문 종류와 접수 규칙이 다릅니다. 게다가 대체거래소 넥스트레이드(NXT)가 생기면서 거래 가능 시간 구간 자체가 예전과 달라졌습니다(NXT·KRX 주문 라우팅 참고). 개장일 판정과 별개로 현재 시각이 주문 허용 구간인지를 확인하는 조건을 따로 두십시오.

③ 연초 개장일은 개장 시각이 다르다

매년 첫 거래일은 개장 시각을 늦추는 것이 관례입니다. 09:00에 시세 조회를 시작하도록 굳게 짜 둔 봇은 이날 빈 데이터를 받고 이상한 판단을 합니다. 시각을 상수로 박기보다 첫 유효 체결이 들어올 때까지 대기하는 구조가 견고합니다. 정확한 개장·폐장 일정은 한국거래소 공식 안내를 확인하십시오.

④ 해외 시장은 완전히 별개다

XKRX 캘린더는 한국 시장만 답합니다. 미국 주식을 함께 돌린다면 XNYS·XNAS 캘린더를 따로 봐야 하고, 서머타임 때문에 한국시간 기준 개장 시각이 연중 두 번 바뀝니다. 이 문제는 미국·해외주식 자동매매 가이드에서 따로 다뤘습니다.

자주 묻는 질문

주말만 걸러도 되지 않나요?

안 됩니다. 대체공휴일·임시공휴일·선거일·연말 폐장일이 남습니다. 특히 임시공휴일은 연중에 새로 지정되므로 하드코딩이 불가능합니다. 매일 아침 한 번 물어보는 구조로 가십시오.

CTCA0903R을 매 루프마다 부르면 안 되나요?

공식 예제가 가급적 1일 1회를 안내합니다. 원장 서비스와 연관돼 있어 단시간 다수 호출이 서비스에 영향을 줄 수 있다는 취지입니다. 결과를 파일에 저장하고 날짜가 바뀔 때만 다시 부르십시오.

조회가 실패하면 어떻게 하나요?

"열린 걸로 치고 진행"은 최악입니다. 알림 한 통을 남기고 주문 없이 종료하십시오. 봇의 기본값은 언제나 아무것도 하지 않는 쪽이어야 합니다. 이런 실패 처리 원칙은 봇 장애 복구 원칙에 정리돼 있습니다.

키움 REST API에도 같은 기능이 있나요?

증권사마다 제공 범위가 다르므로 단정할 수 없습니다. 각사 공식 개발자 문서에서 휴장일·영업일 조회에 해당하는 기능이 있는지 확인하시고, 없다면 exchange_calendars로 판정하되 주문 거부 응답을 정상 흐름으로 처리하는 방어를 함께 두십시오. 키움 쪽 전반은 키움 REST API 가이드를 참고하십시오.

확인 캐치. 이 글의 엔드포인트 경로·tr_id·응답 필드명은 2026년 8월 9일 기준 한국투자증권 공식 깃허브 저장소(koreainvestment/open-trading-api)의 예제 코드를 근거로 정리했습니다. API 스펙과 호출 정책은 증권사 사정으로 예고 없이 변경될 수 있으므로 구현 전 공식 개발자센터에서 최신 값을 반드시 확인하십시오. 휴장일·개장 시각의 공식 기준은 한국거래소 안내를 따르며, 본 글은 특정 종목·수익률에 대한 어떤 예측이나 권유도 담고 있지 않습니다.

마무리

정리하면 세 줄입니다. 실거래 봇의 개장일 판정은 CTCA0903Ropnd_yn으로 하고 하루 한 번만 부른다. 백테스트 거래일은 pykrx, 미래 일정 계산은 exchange_calendarsXKRX로 나눠 쓴다. 모든 날짜 계산에 Asia/Seoul을 명시한다.

전략이 좋아도 "오늘 열리나"를 틀리면 그날 하루가 통째로 망가집니다.

전략만 주시면 운영 배관까지 함께 만듭니다

휴장일 판정·시간대·실패 정지까지 포함해 설계합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기