파이썬 시간대(timezone) 처리 완전정리 — 거래소 API 시간 맞추기
자동매매 봇에서 가장 조용하게, 가장 오래 사람을 괴롭히는 버그는 시간입니다. 에러도 안 나고, 로그도 정상으로 보이는데, 캔들 하나가 밀려 있고 그래서 신호가 9시간 늦게 뜹니다. 이 글은 파이썬에서 시간대(timezone, 지역별 표준시 오프셋)를 다룰 때 반복적으로 밟게 되는 지뢰를 순서대로 정리한 노트입니다.
⚠️ 이 글은 교육·정보 제공 목적입니다. 투자 권유가 아니며, 투자 손실 책임은 본인에게 있습니다.
1. 모든 문제의 뿌리: naive datetime
파이썬 datetime은 두 종류입니다.
- naive — 시간대 정보가 없는 datetime. "10시"라고만 적혀 있고 어디의 10시인지 모릅니다.
- aware —
tzinfo가 붙어 있어 UTC 기준 몇 시인지 확정할 수 있는 datetime.
문제는 naive끼리는 아무 경고 없이 비교·뺄셈이 된다는 점입니다. UTC로 받은 거래소 시각과 KST로 만든 로컬 시각을 naive 상태로 빼면, 결과는 9시간 틀린 채 조용히 계산됩니다.
from datetime import datetime, timezone, timedelta
datetime.now() # naive — 이 서버가 어느 TZ인지에 따라 값이 달라짐
datetime.utcnow() # naive인데 값은 UTC — 최악의 조합, 쓰지 마세요
datetime.now(timezone.utc) # aware UTC ✅
datetime.now(timezone(timedelta(hours=9))) # aware KST ✅
규칙 하나만 지키면 됩니다: 코드 안에서 돌아다니는 datetime은 전부 aware. naive는 외부에서 들어온 문자열을 파싱한 직후 딱 한 지점에서만 존재하고, 즉시 aware로 승격시킵니다.
2. pytz? zoneinfo?
파이썬 3.9부터는 표준 라이브러리 zoneinfo가 있습니다. 외부 패키지 pytz는 이제 필요 없고, 오히려 localize()를 안 쓰면 LMT(지역 평균시)라는 이상한 오프셋이 붙는 함정이 있어 초보자에게 위험합니다.
from zoneinfo import ZoneInfo
KST = ZoneInfo("Asia/Seoul") # 표준 라이브러리, 3.9+
now = datetime.now(KST)
다만 한국은 서머타임이 없어 오프셋이 항상 +09:00으로 고정입니다. 그래서 실무 봇 코드에서는 IANA 데이터베이스 의존성을 아예 없애려고 고정 오프셋을 쓰는 경우도 많습니다.
KST = timezone(timedelta(hours=9)) # tzdata 없는 컨테이너에서도 동작
def _now_kst() -> datetime:
return datetime.now(KST)
둘 중 무엇이든 좋지만, 프로젝트 전체에서 한 곳에 정의하고 import해서 쓰세요. 모듈마다 KST = ...를 복붙해 두면 나중에 하나만 바뀌었을 때 추적이 지옥이 됩니다.
3. 가장 많이 틀리는 곳 — replace(tzinfo=) vs astimezone()
이름이 비슷해서 헷갈리지만, 하는 일이 정반대입니다.
dt.replace(tzinfo=KST)— 주장합니다. 숫자는 그대로 두고 "이건 KST였어"라고 라벨만 붙입니다.dt.astimezone(KST)— 변환합니다. 실제 시각은 유지한 채 숫자를 KST로 바꿉니다.
# 거래소가 준 ISO 문자열 (UTC, 끝에 Z)
raw = "2026-08-06T01:00:00Z"
# ❌ 9시간 틀림 — 01:00을 KST 01:00이라고 우겨버림
wrong = datetime.fromisoformat(raw.replace("Z", "")).replace(tzinfo=KST)
# ✅ UTC로 정확히 파싱 후 KST로 변환 → 10:00 KST
right = datetime.fromisoformat(raw.replace("Z", "+00:00")).astimezone(KST)
판단 기준은 간단합니다. 원본 문자열이 이미 어느 TZ인지 알고 있으면 astimezone, 원본이 TZ 없는 로컬 표기(예: 20260806T100000)라서 내가 알려줘야 하면 replace(tzinfo=)입니다.
4. 거래소 타임스탬프: 초냐 밀리초냐
유닉스 타임스탬프(1970-01-01 UTC부터 흐른 시간)는 거래소마다 단위가 다릅니다. 바이낸스는 밀리초, 어떤 API는 초, 또 어떤 곳은 마이크로초를 줍니다. 1000배 차이라 잘못 해석하면 1970년이나 55000년으로 날아갑니다.
방어법은 자릿수로 자동 판별하는 헬퍼 하나입니다.
def ts_to_kst(ts) -> datetime:
"""초/밀리초/마이크로초 타임스탬프를 KST aware datetime으로."""
v = float(ts)
if v > 1e14: # 마이크로초
v /= 1_000_000
elif v > 1e11: # 밀리초
v /= 1000
return datetime.fromtimestamp(v, tz=timezone.utc).astimezone(KST)
fromtimestamp에 tz=를 꼭 넘기세요. 빼먹으면 서버 로컬 TZ 기준 naive가 나오고, 개발 PC에서는 맞다가 UTC로 도는 서버에 올리는 순간 틀어집니다.
반대 방향, 즉 서명이나 nonce로 밀리초를 만들어 보낼 때는 이렇게 씁니다.
import time
ms = int(time.time() * 1000) # 거래소 signed request의 timestamp 파라미터
5. 실전 사례 — 캔들이 한 칸 밀리는 이유
실제로 겪은 사례입니다. 분봉을 모아 15분봉으로 재집계하는 코드에서, 거래소가 준 시각은 UTC인데 리샘플 기준을 로컬 시간으로 잡았습니다. 결과적으로 09:00~09:15 구간에 08:00대 체결이 섞여 들어갔고, 지표는 정상 범위의 값을 뱉었습니다. 에러가 안 나니 몇 주를 몰랐습니다.
교훈은 두 가지였습니다.
- 집계는 UTC로, 표시는 KST로. 내부 계산·저장·비교는 전부 UTC로 통일하고, 사람이 읽는 순간에만 KST로 변환합니다.
- 로그는 두 벌 다 남긴다. UTC와 KST를 함께 찍으면 시간 관련 버그의 원인 파악 시간이 극적으로 줄어듭니다.
now_utc = datetime.now(timezone.utc)
now_kst = now_utc.astimezone(KST)
log = {
"at_utc": now_utc.isoformat(),
"at_kst": now_kst.strftime("%Y-%m-%d %H:%M:%S KST"),
}
추가로, 파싱 실패에 대한 정책을 미리 정해두세요. 시각 파싱이 터졌을 때 예외로 봇 전체를 죽일지, 보수적인 기본값으로 대체하고 경고만 남길지는 상황에 따라 다릅니다. 다만 말없이 datetime.now()로 대체하는 것만은 피하세요 — 가장 찾기 어려운 종류의 버그가 됩니다.
6. 장 시간 판정에서의 함정
"지금이 장중인가"를 판단하는 함수는 봇의 심장입니다. 그런데 이 함수가 naive datetime.now()를 쓰면, 서버 TZ가 UTC인 클라우드에 배포하는 순간 장중을 새벽으로 착각해 하루 종일 아무것도 안 합니다. 조용히 아무 일도 안 하기 때문에 알림도 안 옵니다.
def is_market_hours() -> bool:
now = datetime.now(KST) # 반드시 aware
if now.weekday() >= 5: # 토·일
return False
return (9, 0) <= (now.hour, now.minute) < (15, 30)
휴장일(공휴일) 처리는 별도 캘린더가 필요합니다. 여기서는 요일만 걸렀지만, 실운영이라면 휴장일 목록을 파일로 두고 주기적으로 갱신하는 편이 안전합니다.
체크리스트
datetime.utcnow()와datetime.now()(인자 없이)를 코드에서 전부 제거- TZ 상수는 프로젝트에 한 번만 정의하고 import
- 외부 문자열 →
astimezone(변환) / TZ 없는 로컬 표기 →replace(tzinfo=)(주장) - 타임스탬프는 자릿수로 초·밀리초·마이크로초 자동 판별
fromtimestamp에는 항상tz=지정- 내부 저장·비교는 UTC, 출력만 KST. 로그는 두 벌 다
- 장 시간 판정 함수는 반드시 aware datetime 사용
시간 버그는 터질 때 요란하지 않습니다. 그래서 더 비쌉니다. 처음 설계할 때 30분 들여 규칙을 정해두는 게, 나중에 며칠을 아껴줍니다.
댓글
댓글 쓰기