키움 opt10080으로 분봉을 캐시에 모아두고 재사용하는 수집기를 쓰고 있다면, 이 질문을 해봐야 한다 — "캐시를 건너뛰는 조건이 시작일만 검사하고 있지 않은가?" 그렇다면 일부 종목은 과거 어느 날짜에 멈춘 채, 재실행해도 영영 최신 데이터를 받아오지 않는다. 1편(키움 -209 해결기)과 같은 수집기에서 잡은, 정반대 방향의 버그다.
바쁜 사람을 위한 3줄 요약
- 증상: 일부 종목의 분봉 캐시가 과거 날짜에 멈춘 채, 재실행해도 "캐시재사용"으로 건너뛰어진다.
- 원인: 캐시 생략 조건이 "시작일까지 데이터가 있나"(min)만 검사하고 "최신까지 있나"(max)는 안 본다.
- 수정: 시작일·종료일 양방향 검사(
reach_back AND reach_fwd)로 바꾸고, 부족분만 받아 병합한다.
1편과의 관계
1편의 버그는 "너무 많이 받는" 문제였다 — 캐시가 있는데도 매번 전체 기간을 다시 받아 -209에 걸렸다. 이번 글의 버그는 "아예 안 받는" 문제다 — 캐시가 있다는 이유로 조회를 건너뛰는데, 그 판단이 틀려서 최신 구간이 영영 비어 있게 된다.
솔직하게 말하면 시간순으로는 이 버그를 먼저 발견했다. 이걸 고치자 멈춰 있던 종목들이 일제히 대량 재조회를 시작했고, 그 조회량 폭증이 -209를 불렀고, 그래서 1편의 증분 조회를 만들게 됐다. 두 글을 합쳐야 수집기 하나가 완성된다.
환경
- Python 3.10 (32bit) + PyQt5 + 키움 OpenAPI
- TR:
opt10080(주식분봉차트조회) - 종목별 CSV 캐시 + 재실행 시 캐시 재사용 구조
1. 증상
백테스트 기간을 6월 말까지 늘리려고 수집기를 돌렸다. 종료일을 6/26으로 지정했으니 전 종목이 6/26까지 채워져야 한다. 그런데 수집이 "정상 완료"된 뒤 캐시를 확인해보니 이상했다. (아래는 cache 폴더의 종목별 CSV에서 마지막 날짜만 뽑아, 날짜별로 몇 종목인지 세는 확인용 한 줄이다.)
import pandas as pd, glob
from collections import Counter
print(Counter(str(pd.read_csv(f)['datetime'].max())[:10]
for f in glob.glob('cache/*.csv')))
{'2026-06-25': ..., '2026-06-05': ...} # 종목별 마지막 날짜가 제각각
일부 종목은 6/25까지 있는데, 일부는 6/5에서 멈춰 있다. 더 이상한 건 재실행해도 똑같다는 것. 6/5에서 멈춘 종목이 로그에 "캐시재사용"으로 찍히며 건너뛰어진다. 에러도 없다. 수집기는 자기가 일을 다 했다고 믿고 있다.
이게 백테스트에서 왜 치명적이냐면 — 데이터가 6/5까지밖에 없는 종목은 6월 중순 이후 구간에서 신호 자체가 계산되지 않는다. 백테스트는 돌아가고 결과도 나오지만, 그 종목들이 조용히 빠진 결과다. 에러가 나는 버그보다 이런 "조용히 틀리는" 버그가 훨씬 위험하다.
2. 원인: 캐시 생략 조건이 시작일만 검사한다
_get_bars의 캐시 재사용 판단 코드다.
# [수정 전] _get_bars — 캐시 생략 조건
if (not self.ignore_cache.isChecked()) and existing is not None and not existing.empty:
if existing["datetime"].min() <= start:
return existing, False # 캐시로 충분 → 재조회 생략
조건이 existing["datetime"].min() <= start 하나다. 번역하면 "캐시의 가장 과거 데이터가 시작일까지 닿아 있으면 충분하다"는 판단이다. 캐시의 최신 쪽(max)이 어디까지 있는지는 안 본다.
그림으로 보면 상황이 명확해진다. 6/5에서 멈춘 종목의 시간축이다.
과거 ─────────────────────────────────────────────► 최신
시작일(1월) 6/5 목표(6/26)
├─────────── 캐시 보유 구간 ─────────┤ ‥‥ 빈 구간 ‥‥┤
reach_back (과거 방향): 시작일까지 닿았나? → ✅ 닿음
reach_fwd (최신 방향): 목표일까지 닿았나? → ❌ 6/5에서 멈춤
[수정 전] reach_back만 검사 → "충분" 판정 → 생략 → 빈 구간 영영 방치
[수정 후] 둘 다 검사 → 빈 구간(6/5 이후)만 받아 이어붙임
캐시가 "충분한가"라는 질문에는 방향이 두 개 있는데, 수정 전 코드는 왼쪽(과거) 방향만 확인하고 있었던 것이다.
그래서 이런 일이 생긴다. 6/5까지 수집된 종목은, 과거 방향으로는 시작일(1월)까지 데이터가 있으니 조건을 통과한다 → 생략 → 6/6 이후는 영영 안 받는다. 재실행을 백 번 해도 같다.
왜 이런 코드가 됐는지도 짚을 만하다. 수집기를 처음 만들 때의 관심사는 "과거로 충분히 깊게 받았는가"였다(볼린저 워밍업 등 과거 데이터 확보가 목적이었으니까). 그때는 수집 시점 = 최신 시점이라 max를 검사할 이유가 없었다. 캐시를 이어 쓰기 시작하면서 "최신까지 닿았는가"라는 두 번째 축이 생겼는데, 생략 조건이 따라가지 못한 것이다.
3. 수정: 시작일과 종료일, 둘 다 닿아야 생략
생략 조건을 두 방향 검사로 바꾼다.
# [수정 후] _get_bars — 시작일(min) + 종료일(max) 둘 다 검사
def _get_bars(self, code, start, target_end=None):
...
if (not self.ignore_cache.isChecked()) and existing is not None and not existing.empty:
reach_back = existing["datetime"].min() <= start
reach_fwd = True
if target_end is not None:
reach_fwd = (existing["datetime"].max().normalize()
>= pd.Timestamp(target_end).normalize())
if reach_back and reach_fwd:
return existing, False # 둘 다 충분할 때만 생략
...
# 부족하면 조회 후, 기존 캐시와 union 병합 (부분 조회로 덮어써도 깊이 손실 없음)
if existing is not None and not existing.empty:
df = (pd.concat([existing, df])
.drop_duplicates(subset=["datetime"])
.sort_values("datetime").reset_index(drop=True))
호출부에서는 "어디까지 채워야 완성인지"의 기준일(target_end)을 정해서 넘긴다. UI에서 종료일을 지정했으면 그 날짜, 아니면 오늘이다.
# [수정 후] 수집 루프 시작부 — 증분 기준일 결정
if self.use_end.isChecked():
target_end = pd.Timestamp(self.end_date.date().toString("yyyy-MM-dd"))
else:
target_end = pd.Timestamp.now().normalize()
self.log(f"[증분 기준] 캐시가 {target_end.date()}까지 안 닿은 종목은 이어받기")
...
df, hit = self._get_bars(code, start, target_end)
설계 포인트 세 가지.
- 날짜 단위 비교(
normalize()): 분봉 캐시의 마지막 시각은 15:19처럼 장중 시각일 수 있다. 시각까지 비교하면 "그날 데이터가 있는데도 부족 판정"이 나므로 날짜로 정규화해서 비교한다. target_end=None이면 옛 동작 유지: 종료일 개념이 없는 기존 호출부는 그대로 돈다(하위호환). 1편의fetch_back_to와 같은 원칙 — 새 기능은 옵션 파라미터로, 기본 동작은 보존.- 병합은 union: 부족분만 받아 이어붙일 때 기존 캐시를 덮어쓰면 과거 깊이가 손실될 수 있어, concat 후 datetime 중복 제거로 합친다.
4. 검증
4-1. 생략 판단 로직 5케이스 테스트
핵심인 "생략할 것인가" 판단만 따로 떼어 합성 케이스로 검증했다.
def should_skip(cache_min, cache_max, start, target_end, ignore_cache):
if ignore_cache: return False
reach_back = pd.Timestamp(cache_min) <= pd.Timestamp(start)
reach_fwd = True
if target_end is not None:
reach_fwd = (pd.Timestamp(cache_max).normalize()
>= pd.Timestamp(target_end).normalize())
return reach_back and reach_fwd
cases = [
# (설명, cache_min, cache_max, start, target_end, ignore, 기대_skip)
("옛종목 6/5멈춤, 6/25목표 → 이어받기", "2025-12-08","2026-06-05","2026-01-15","2026-06-25",False, False),
("6/25까지 있음 → 생략", "2025-12-08","2026-06-25","2026-01-15","2026-06-25",False, True),
("시작일 못닿음 → 재조회", "2026-02-01","2026-06-25","2026-01-15","2026-06-25",False, False),
("캐시무시 ON → 무조건 받기", "2025-12-08","2026-06-25","2026-01-15","2026-06-25",True, False),
("종료일 미지정(None) → 옛 동작 유지", "2025-12-08","2026-06-05","2026-01-15",None, False, True),
]
✅ 옛종목 6/5멈춤, 6/25목표 → 이어받기: skip=False (기대 False)
✅ 6/25까지 있음 → 생략: skip=True (기대 True)
✅ 시작일 못닿음 → 재조회: skip=False (기대 False)
✅ 캐시무시 ON → 무조건 받기: skip=False (기대 False)
✅ 종료일 미지정(None) → 옛 동작 유지: skip=True (기대 True)
버그였던 1번 케이스(멈춘 종목)가 이어받기로 바뀌고, 나머지 동작은 전부 보존된다.
4-2. 실제 수집 후 정합성 확인
수정본으로 수집을 완주한 뒤, 증상 확인에 썼던 Counter one-liner를 그대로 다시 돌렸다.
{'2026-06-26': 633}
전 종목이 한 날짜로 모였다. 이 one-liner는 수정 검증용으로 쓰고 끝낼 게 아니라, 매 수집 후의 상시 점검 루틴으로 쓰는 걸 권한다. 날짜가 흩어져 있으면 뭔가 덜 받은 것이고, 백테스트를 돌리기 전에 그걸 알 수 있다.
5. 주의사항
수정 직후 첫 실행은 오래 걸리고, 조회량이 폭증할 수 있다. 그동안 멈춰 있던 종목들이 밀린 구간을 한꺼번에 받기 때문이다. 실제로 나는 이 수정 직후의 대량 재조회에서 키움 -209(조회횟수 제한)를 만났고, 그 해결 과정이 1편이다. 이 글의 수정을 적용한다면 1편의 증분 조회(fetch_back_to)도 같이 적용하는 걸 권한다. 둘은 한 세트다.
"캐시 무시" 옵션과 혼동하지 마라. 캐시 무시는 전부 다시 받는 강제 초기화이고, 이 수정은 부족한 종목만 부족한 만큼 받는 것이다. 날짜가 안 맞는다고 캐시 무시를 켜는 건 -209로 가는 지름길이다.
요약
| 1편의 버그 | 이 글의 버그 | |
|---|---|---|
| 방향 | 너무 많이 받음 | 아예 안 받음 |
| 원인 | 캐시가 "어디까지 받을지"에 반영 안 됨 | 생략 조건이 시작일(min)만 검사 |
| 증상 | -209 조회 차단 | 종목이 과거 날짜에 멈춘 채 조용히 방치 |
| 수정 | fetch_back_to (캐시 최신까지만 거슬러 받기) |
reach_back AND reach_fwd (양방향 검사) |
| 검증 | 조회 횟수 87% 감소 + 완주 로그 | 5케이스 테스트 + Counter 한 날짜 수렴 |
캐시 재사용 로직을 만들 때의 교훈은 이거다. "충분한가"의 판단에는 항상 두 방향이 있다 — 과거로 충분한가, 그리고 최신까지 충분한가. 한쪽만 검사하는 캐시는 언젠가 반대쪽에서 조용히 구멍이 난다.
이 시스템의 실매매 기록은 유튜브에서 → https://www.youtube.com/@quant-one-ai
📖 3년 기록을 전자책으로 정리 중입니다 — 목차·샘플 공개: (https://oneai.tistory.com/6)
One AI : AI 자동매매 시스템 만들기
이 채널은 AI와 규칙 기반 시스템으로 운영하는 퀀트매매, 자동매매의 실전 기록 채널입니다. 매매 신호, 실행, 결과, 복기 과정을 영상으로 정리합니다. 특정 종목 추천이 아닌 개인 매매 기록
www.youtube.com