Skip to content

fix: 스냅샷 커버리지 미달의 진짜 원인은 결측이 아니라 거래일 달력이었다 - #459

Merged
easygap merged 1 commit into
mainfrom
fix/snapshot-coverage-recovery
Aug 27, 2026
Merged

fix: 스냅샷 커버리지 미달의 진짜 원인은 결측이 아니라 거래일 달력이었다#459
easygap merged 1 commit into
mainfrom
fix/snapshot-coverage-recovery

Conversation

@easygap

@easygap easygap commented Aug 27, 2026

Copy link
Copy Markdown
Owner

시작점

승격 기준인 스냅샷 커버리지 95%가 두 트랙 다 미달로 나왔다. 결측을 어떻게 복구할지 파다가, 큰 쪽이 결측이 아니라는 걸 알았다.

진짜 원인 — 달력 한 줄

config/holidays.yaml2026-07-17이 빠져 있었다. 휴장일인데 거래일로 집계되니 분모만 부풀어 실제보다 낮게 보였다.

트랙 잘못된 달력 실제 시장 달력
kr_diversified_hold 52/55 = 94.5% 52/54 = 96.3%
kr_pocket 31/33 = 93.9% 31/32 = 96.9%

한 줄이 두 트랙을 기준 아래로 끌어내렸다. 근거는 KOSPI 지수와 005930·105560·069500 전부 그날 봉이 없고 07-16·07-20은 있다는 것. pykrx 영업일 API가 이 환경에서 깨져 있어(RangeIndex 오류) 자동 갱신이 조용히 놓쳤다.

대조 도구를 만들었더니 7건이 더 나왔다

tools/verify_trading_calendar.py — 달력 판정을 KOSPI 지수 데이터와 대조한다.

추가: 02-16·02-17·02-18 (설 연휴), 05-01 (근로자의 날), 06-03 (지방선거), 07-17
제거: 01-27·01-28·01-29 — 이 날들은 실제로 개장했다

설 연휴가 2025년 날짜로 들어가 있었다. 지금은 2026년 전 구간(평일 170일)이 시장 데이터와 일치한다.

달력 오류는 양방향으로 조용히 해롭다. 휴장일을 거래일로 오인하면 분모가 부풀어 없는 결측을 쫓게 되고, 거래일을 휴장일로 오인하면 진짜 결측이 집계에서 사라진다. 뒤쪽이 더 위험하다. 그래서 달력을 신뢰하지 않고 지수 데이터로 직접 확인하는 쪽을 택했다.

진짜 결측 2일은 원장 재생으로 복원

core/snapshot_backfill.py — 06-26, 08-18.

정확히 복원되는 것: 포지션과 현금. trade_history + cash_flows 재생이라 근사가 아니다. 실측 81일치를 재구성해 대조하니 현금은 전부 일치했다.

근사에 그치는 것: NAV. 기존 스냅샷이 10:07 장중 마크인데 그 시점 틱이 남아 있지 않다. 어떤 방식으로도 정확히는 못 맞춘다.

가격 기준 평균 오차 최대
시가+종가 중간값 0.45~0.53% 1.9%
시가 0.59~0.65% 2.1%
종가 0.79~0.81% 3.2%

중간값을 쓴다. 그리고 복원분은 reconstructed=True로 표시하고 평가가 실측과 나눠 표기한다.

스냅샷 커버리지: 100% (실측 52일 96% + 사후 복원 2일)

커버리지 게이트는 '시스템이 실제로 돌았는가'를 보는 장치다. 보정분이 실측인 척하면 게이트가 목적을 잃는다. 실측이 나중에 들어오면 표시를 걷어낸다. 가격을 못 구하는 날은 채우지 않는다.

앞으로의 결측은 사이클이 스스로 메운다

_nav_attribution_date가 '오늘이 거래일이면 오늘'로 귀속해서, 어제 사이클이 안 돌면 그 하루는 영원히 비었다. 08-18 결측이 08-19~08-26 내내 남은 이유다.

이제 매매 전에 최근 10일 내 결측을 보충한다. 소급 한도를 짧게 잡은 건 며칠짜리 사고는 메우되 장기 중단은 공백으로 드러나야 하기 때문이다.

결과

kr_diversified_hold  커버리지 100% (실측 96% + 복원 2일)  이슈 0건
kr_pocket            커버리지 100% (실측 97% + 복원 1일)  이슈 0건

달력 교정만으로 이미 기준을 넘었고 복원은 여유분이다.

CI 환경 재현(config/settings.yaml 치움) 전체 스위트 1982개 통과.

달력이 바뀌며 기존 테스트 2건의 기대값이 어긋났다. 둘 다 로직 회귀가 아니라 달력 정정 결과라 기대값을 고쳤다. 연휴 stale 테스트는 1/2729(실제 개장)를 쓰고 있어서 실제 설 연휴(2/1618) 구간으로 옮겼다.

승격 기준인 커버리지 95%가 두 트랙 다 미달로 나와 결측 복구를 파다가, 큰 쪽이
결측이 아니라는 걸 알았다.

config/holidays.yaml에 2026-07-17이 빠져 있었다. 그 날은 휴장인데 거래일로
집계되니 분모만 부풀어 실제보다 낮게 보였다.

  kr_diversified_hold  96.3% → 94.5%
  kr_pocket            96.9% → 93.9%

한 줄이 두 트랙을 기준 아래로 끌어내린 것이다. 근거는 KOSPI 지수와 005930·105560·
069500 전부 그날 봉이 없고 07-16·07-20은 있다는 것. pykrx 영업일 API가 이 환경에서
깨져 있어(RangeIndex) 자동 갱신이 조용히 놓쳤다.

달력을 시장 데이터와 대조하는 도구를 만들어 전 구간을 돌렸더니 7건이 더 나왔다.
설 연휴가 2025년 날짜(01-27~29)로 들어가 있었고 실제 2026년 설(02-16~18)과
근로자의 날(05-01), 지방선거(06-03)가 빠져 있었다. 01-27~29는 실제로 개장했다.
전부 바로잡아 2026년 전 구간이 시장 데이터와 일치한다.

달력 오류는 양방향으로 조용히 해롭다. 휴장일을 거래일로 오인하면 분모가 부풀어
없는 결측을 쫓게 되고, 거래일을 휴장일로 오인하면 진짜 결측이 집계에서 사라진다.
뒤쪽이 더 위험하다. 그래서 달력을 신뢰하지 않고 지수 데이터로 직접 확인한다.

진짜 결측은 2일이었다(06-26, 08-18). 원장 재생으로 복원한다:
 - 포지션·현금은 trade_history + cash_flows 재생으로 정확히 복원된다.
   실측 81일치를 재구성해 대조하니 현금은 전부 일치했다.
 - NAV는 정확히 복원 못 한다. 기존 스냅샷은 10:07 장중 마크인데 그 시점 틱이 없다.
   시가·종가 중간값이 실측에 가장 가까웠다(평균 오차 0.45~0.53%, 시가 0.6%, 종가 0.8%).
 - 그래서 복원분은 reconstructed=True로 표시하고 평가가 실측과 나눠 표기한다.
   커버리지 게이트는 '시스템이 실제로 돌았는가'를 보는 장치라, 보정분이 실측인 척하면
   게이트가 목적을 잃는다. 실측이 나중에 들어오면 표시를 걷어낸다.
 - 가격을 못 구하는 날은 채우지 않는다(가짜 NAV 방지).

앞으로의 결측은 사이클이 스스로 메운다. _nav_attribution_date가 '오늘이 거래일이면
오늘'로 귀속해서 어제 사이클이 안 돌면 그 하루는 영원히 비었다 — 08-18 결측이
08-19~08-26 내내 남은 이유다. 이제 매매 전에 최근 10일 내 결측을 보충한다.
소급 한도를 짧게 잡은 건 며칠짜리 사고는 메우되 장기 중단은 공백으로 드러나야
하기 때문이다.

결과: 두 트랙 다 커버리지 100%(실측 96%/97% + 복원 2일/1일), 이슈 0건.
달력 교정만으로도 이미 기준을 넘었고 복원은 여유분이다.

달력이 바뀌며 기존 테스트 2건의 기대값이 어긋났다. 둘 다 로직 회귀가 아니라
달력 정정 결과라 기대값을 고쳤다. 연휴 stale 테스트는 1/27~29(실제 개장)를 쓰고
있어서 실제 설 연휴(2/16~18) 구간으로 옮겼다.
@easygap
easygap merged commit 6ff13e4 into main Aug 27, 2026
1 check passed
@easygap
easygap deleted the fix/snapshot-coverage-recovery branch August 27, 2026 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant