2026. 10. 28. 00:11ㆍ카테고리 없음
화면 뒤의 데이터 판단 · 아키텍처 · processor/freshness.py
processor/freshness.py — "이 데이터, 오늘 것 맞나?"를 69줄로 판정하기
Passive 의 화면 대부분은 하루 한 번 배치가 계산해 둔 값을 읽습니다. 그런데 배치가 실패하면 화면은 에러 없이 어제 값을 그대로 보여 줍니다. 겉으로는 멀쩡하니 아무도 모릅니다. processor/freshness.py 는 이 문제를 "데이터의 날짜가, 지금쯤 나와 있어야 할 최신 영업일보다 옛날인가?"라는 질문 하나로 바꿔 판정하는 69줄짜리 파일입니다.
1. 흐름의 어디에 있나
📍 적용 화면 — 화면에 직접 노출되지 않음(API·스케줄러·감시 페이지가 공통으로 부르는 판정기).
| ① 지수 시세 테이블 · index_price_raw — 지역별 지수 일봉 |
| ↓ 등락률이 0 이 아닌 가장 최근 날짜 1건 |
| ② 기준일 정하기 · expected_trading_date() — 30초 메모 + 미국 새벽 보정 |
| ↓ 기준일과 데이터 날짜를 비교 |
| ③ 판정 · is_stale() (참/거짓) · freshness_meta() (응답용 꼬리표) |
| ↓ |
| ④ 쓰는 곳 · API 응답 꼬리표 · 자가복구 잡 · 저장 거부 · 관리자 감시 페이지 |
그림 1. 기준일 → 판정 → 쓰는 곳. ②③ 이 이 파일, ④ 는 4장에서 정리.
2. 기준일 정하기 — "시장이 실제로 열린 마지막 날"
📍 적용 화면 — 화면에 직접 노출되지 않음(관리자 감시 페이지 /data-status 의 "기대 영업일" 칸에만 표시).
기준일은 달력이 아니라 지수 시세에서 가져옵니다. database/repositories.py 의 fetch_last_trading_date 가 index_price_raw 에서 등락률이 0 이 아닌 가장 최근 날짜 한 건을 돌려줍니다. 주말·휴장일은 갱신이 원래 멈추므로 자동으로 직전 영업일이 기준이 되고, 휴장일에 날짜만 채워진 행(등락률 0)은 걸러집니다. 종목 주가가 아니라 지수만 봅니다.
| index_price_raw 의 날짜 | 등락률 | 기준일 후보? |
| 10.26 (월, 휴장 가정) | 0.00% | 제외 — 날짜만 있는 행 |
| 10.23 (금) | +0.42% | 기준일 |
| 10.22 (목) | -0.15% | 더 옛날 |
그림 2. 기준일 고르기(예시 값). 등락률 0 인 행을 건너뛰고 가장 최근 실거래일을 고릅니다.
미국 새벽 보정 — 한국 시간으로는 날짜가 바뀌었어도 뉴욕은 아직 새벽일 수 있습니다. 뉴욕 평일 04:00(프리장 시작) 전인데 지수 테이블에 "오늘" 행이 먼저 들어와 있으면, 기준일을 직전 평일로 낮춥니다. 아직 생길 수 없는 데이터를 기다리며 "오래됨"을 외치는 오판을 막는 장치입니다.
processor/freshness.py · expected_trading_date() · 23~48줄
# 지금 시각(초)을 잰다
now = time.time()
# 이 지역의 기준일을 30초 안에 구해 둔 적이 있는지 메모장을 본다
cached = _EXP_CACHE.get(region)
# 30초가 안 지났으면 DB 를 다시 읽지 않고 그 값을 돌려준다
if cached and (now - cached[0]) < _EXP_TTL:
return cached[1]
# 지수 시세에서 실거래가 있었던 최신 날짜를 읽는다
try:
exp = fetch_last_trading_date(region)
# 읽기 실패면 기준일을 '모름'으로 둔다
except Exception:
exp = None
# 미국이고 기준일이 있으면 뉴욕 새벽 보정을 한다
if region == 'us' and exp:
try:
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
# 뉴욕 현재 시각
now_ny = datetime.now(ZoneInfo('America/New_York'))
# 자정부터 몇 분 지났나
mins = now_ny.hour * 60 + now_ny.minute
# 평일 04:00 전인데 기준일이 '뉴욕 오늘'이면
if now_ny.weekday() < 5 and mins < 4 * 60 and exp == now_ny.date().isoformat():
# 하루 전으로 물러나서
prev = now_ny.date() - timedelta(days=1)
# 주말이면 금요일까지 더 물러난다
while prev.weekday() >= 5:
prev -= timedelta(days=1)
# 그 직전 평일을 기준일로 쓴다
exp = prev.isoformat()
# 시간대 계산이 실패해도 원래 기준일을 그대로 쓴다
except Exception:
pass
# 구한 기준일을 시각과 함께 메모장에 적는다
_EXP_CACHE[region] = (now, exp)
# 기준일을 돌려준다
return exp
30초 메모는 한 요청 안에서 여러 데이터를 연달아 검사할 때 같은 DB 조회가 반복되지 않게 하는 장치입니다. 기준일은 하루에 한 번 바뀌는 값이라 30초 동안 재사용해도 틀릴 일이 거의 없습니다.
3. 판정 — 날짜 글자를 그대로 비교한다
📍 적용 화면 — 화면에 직접 노출되지 않음(아래 4장의 API·잡이 이 결과로 움직임).
날짜는 모두 YYYY-MM-DD 글자입니다. 이 형식은 글자 순서와 날짜 순서가 같아서 "2026-10-22" < "2026-10-23" 처럼 날짜로 바꾸지 않고 바로 비교할 수 있습니다. 판정에서 중요한 건 "모를 때" 어느 쪽으로 기울이느냐입니다.
| 기준일 | 데이터 날짜 | 판정 | 이유 |
| 10.23 | 10.23 | 신선 | 같은 날 |
| 10.23 | 10.22 | 오래됨 | 기준일보다 앞섬 |
| 10.23 | 없음 | 오래됨 | 빈 데이터는 갱신 실패로 본다 |
| 모름 | 무엇이든 | 신선(보류) | 자를 잃었으면 재지 않는다 |
그림 3. 네 가지 경우(예시 날짜). 마지막 줄이 이 파일의 핵심 선택입니다.
processor/freshness.py · is_stale() · 57~62줄
# 이 지역의 기준일을 구한다
exp = expected_trading_date(region)
# 기준일을 모르면 판정을 보류하고 '신선'으로 둔다
if exp is None:
return False
# 데이터 날짜가 없으면(빈 데이터) '오래됨'
if not row_date:
return True
# 날짜 글자가 기준일보다 앞서면 '오래됨'
return str(row_date) < exp
왜 "모르면 신선"인가 — 지수 테이블이 끊긴 날 "모르면 오래됨"으로 판정하면 모든 데이터가 한꺼번에 오래됨이 되고, 아래 자가복구 잡이 멀쩡한 데이터까지 전부 다시 계산하러 달려듭니다. 그래서 기준을 잃으면 검사를 쉬는 쪽을 택했습니다. 대신 그날은 진짜 지연도 잡지 못한다는 대가가 있습니다(5장).
freshness_meta()(65~69줄)는 같은 판정을 {as_of: 데이터 날짜, expected: 기준일, stale: 오래됨 여부} 묶음으로 만들어 API 응답에 붙입니다.
4. 판정 결과를 쓰는 네 곳
📍 적용 화면 — 시장 탭(#tab-market) · 펀더멘털(#tab-fundamental) · 시장 분위기(#tab-signal) · AI차트(#tab-chart) 상단의 "기준일" 라벨, 그리고 관리자 감시 페이지.
| 쓰는 곳 | 코드 | 판정 결과로 하는 일 |
| API 응답 꼬리표 | api/routers/ regime · anomaly · chart · macro · market_driver · sector_cycle | freshness_meta 를 응답에 붙임 → 프론트는 as_of 만 읽어 "날짜 기준" 회색 라벨로 표시(static/js/main.js _asofLabelHtml) |
| 자가복구 잡 | scheduler/freshness_job.py verify_freshness | 오래된 데이터셋만 그 재계산 함수를 1회 호출(heal). 미국 화~토 08:30 · 한국 월~금 18:30(KST) 크론 + 파이프라인 말미 |
| 요청 중 즉시 폴백 | api/routers/macro.py 공포탐욕 지수 | 미국 값이 오래됐으면 CNN 을 다시 조회 → 새 값도 신선할 때만 교체·저장, 아니면 직전 값 유지 |
| 관리자 감시 | api/routers/admin_status.py | 데이터 지연 대시보드에 지역별 기대 영업일을 표시 |
표 1. 호출 위치는 각 파일에서 processor.freshness import 로 확인. 이 밖에 섹터 강약 배치(processor/sector_rrg.py)는 오래된 결과면 저장하지 않고 기존 캐시를 지킵니다.
화면에는 "오래됨" 경고가 없습니다. 응답에는 stale 이 실려 있지만 프론트 라벨 함수는 날짜만 그립니다(static/js/home.js 의 주석도 "지연 경고 없음"). 사용자에게는 사실(기준일)만 보여 주고, "오래됨"은 서버가 스스로 고치는 신호로만 씁니다.
자가복구에도 브레이크가 있습니다. 같은 (지역 · 데이터셋 · 기준일) 조합은 프로세스당 한 번만 heal 합니다. 원본 소스 자체가 죽은 날 무한히 다시 시도하지 않기 위해서입니다. 서버에서 다시 만들 수 없는 차트 30일 예측(TimesFM 추론, GitHub Actions 에서만 실행)은 heal 대상에서 빼고 지연만 기록합니다.
| 크론 08:30 · 데이터셋별 최신 날짜 읽기(DB 조회만, 외부 호출 0) |
| ↓ is_stale |
| 오래된 것만 · 이미 오늘 heal 했나? → 아니면 재계산 1회 |
| ↓ |
| 다시 판정 · healed / heal-fail 기록 → 다음 영업일(기준일 변경)에 가드 해제 |
그림 4. 자가복구 잡 한 바퀴. 검사는 싸고(DB 읽기), 비싼 재계산은 오래된 것만 한 번.
5. 이 자로 잴 수 없는 것
📍 적용 화면 — 화면에 직접 노출되지 않음(설계 한계).
| 상황 | 어떻게 다루나 |
| 원래 며칠 늦게 나오는 데이터 (예: 신용잔고) |
이 파일을 쓰지 않습니다. processor/credit_leverage.py 가 "영업일 N일까지 늦어도 정상"인 자체 판정을 쓰고, 감시 페이지는 processor/freshness_spec.py 의 max_lag_bdays(허용 지연 영업일)로 봐줍니다. |
| 지수 테이블 자체가 끊김 | 기준일이 "모름"이 되어 모든 판정이 보류(신선)됩니다. 그날은 다른 데이터의 지연도 이 파일로는 드러나지 않습니다. |
| 날짜는 최신인데 값이 빈 행 | 날짜 비교로는 못 잡습니다. 그래서 freshness_job.py 의 매크로 검사는 핵심 값(VIX·금리)이 비었으면 따로 오래됨으로 봅니다. |
정리
| 자 | 지수 시세에서 등락이 있었던 마지막 날 = 기준일(미국은 뉴욕 새벽 보정). |
| 판정 | 데이터 날짜 < 기준일이면 오래됨, 빈 데이터도 오래됨, 기준을 모르면 보류. |
| 쓰임 | 화면엔 기준일 라벨만, "오래됨"은 자가복구 잡·즉시 폴백·저장 거부의 신호. |
| 범위 | 매일 갱신되는 데이터 전용. 원래 늦는 데이터는 따로 판정. |
이 글의 데이터는 투자 참고용 정보이며 수익을 보장하지 않습니다.
주식 데이터 분석 앱 → dinsightlab.com