같은 날짜가 두 개의 다른 날이 될 때

2026-07-15

코드에서 "어제"를 구하는 가장 흔한 방법은 오늘 날짜에서 하루를 빼는 것입니다. 대부분의 상황에서 이건 옳습니다.

주식 데이터에서는 옳지 않습니다. 월요일의 "어제"는 일요일이 아니라 금요일이고, 연휴 다음 거래일의 "어제"는 나흘 전일 수도 있습니다. 금융 데이터에서 '어제'는 자명한 개념이 아닙니다. 거래일 달력을 봐야만 알 수 있습니다.

이 사실을 안다고 생각했는데, 같은 뿌리에서 나온 사고를 세 번 겪었습니다. 하나는 실제로 터졌고, 둘은 운으로 살아남았습니다. 셋 다 이 글을 준비하는 분석 과정에서 드러났습니다.

사고 1 — 터진 것: 연휴가 만든 유령 날짜

급등 랭킹을 분석하다가, 어느 종목이 2025년 10월 6일에 30일 상승률 1위였다는 기록을 봤습니다. 그 종목의 시총을 같은 날 시장 대표주와 비교하려고 그날 데이터를 다시 꺼냈습니다.

그런데 그날, 거래된 종목이 하나도 없었습니다. 10월 6일은 추석 연휴였습니다.

거래가 0건인 날에 "상승률 1위"가 존재했습니다. 아무도 거래하지 않은 날의 순위표입니다. 유령이 1등을 하고 있었습니다.

무슨 일이 있었는지 추적했습니다. 며칠 이상 이어지는 연휴를 데이터 수집 배치가 처리할 때, 값 자체는 연휴 직전 마지막 거래일(10월 2일)의 데이터로 채웠습니다. 여기까지는 합리적입니다 — 가장 최근의 실제 가격을 쓴 것이니까요. 문제는 그렇게 채운 행에 날짜 라벨만 요청받은 값(10월 6일) 그대로 붙였다는 것입니다.

그래서 데이터의 내용은 10월 2일인데 이름표는 10월 6일인, 정체가 어긋난 행이 만들어졌습니다. 값은 정확했습니다. 라벨이 거짓말을 했을 뿐입니다.

이걸 어떻게 확인했는가가 중요합니다. 그날 시장 대표주의 시가총액을 종가로 나눠 발행주식수를 역산했더니, 그 값이 10월 2일의 발행주식수와 정확히 일치했습니다. 10월 6일의 다른 날 데이터와는 맞지 않았습니다. 내용이 어느 날의 것인지는, 라벨이 아니라 데이터 안에 남은 지문으로 판별할 수 있었습니다.

사고 2 — 안 터진 것: 일요일 앵커

다른 분석에서, 30~35일 간격으로 열한 개의 기준 날짜(앵커)를 잡아 구간 수익률을 계산하고 있었습니다. 그중 하나가 2026년 6월 14일, 일요일이었습니다. 당연히 거래가 없는 날입니다.

이번에는 앵커를 실제 거래일로 스냅하는 처리가 있었습니다. 비거래일이면 가장 가까운 거래일로 밀어주는 로직입니다. 6월 14일은 6월 15일(월)로 해소됐고, 계산은 정상이었습니다.

검증도 통과했습니다. 그 구간들의 지수 수익률을 복리로 이어붙인 값이 기간 전체의 지수 등락과 정확히 맞아떨어졌으니까요. 안전했습니다.

다만 안전했던 이유가 실력이 아니라 운이었습니다. 6월 14일이 일요일이었던 덕에, 가장 가까운 거래일이 다음 날(월요일)이라는 게 명백했습니다. 앞뒤 어디를 봐도 그보다 가까운 거래일이 없었습니다. 만약 앵커가 연휴 한복판에 떨어졌다면, "가장 가까운 거래일"이 앞이냐 뒤냐에 따라 다른 날로 해소될 여지가 있었습니다.

사고 3 — 안 터진 것: 역할에 따라 달라지는 tolerance

사고 2의 그 스냅 로직에는 더 깊은 결함이 숨어 있었습니다.

각 앵커는 구간의 시작점이면서 동시에 다른 구간의 끝점입니다. 앵커 여러 개를 연속으로 이으면, 두 번째 앵커는 첫 구간의 끝이자 둘째 구간의 시작입니다. 하나의 날짜가 두 개의 역할을 겸합니다.

그런데 이 스냅 로직은 역할마다 다른 허용 범위(tolerance)를 쓰고 있었습니다. 끝점으로 해소할 때와 시작점으로 해소할 때 허용하는 날짜 범위가 달랐습니다.

같은 앵커가, 끝점일 때는 A라는 거래일로, 시작점일 때는 B라는 다른 거래일로 해소될 수 있는 구조였습니다. 하나의 날짜가 문맥에 따라 두 개의 서로 다른 날이 됩니다. 이러면 이어붙인 구간들 사이에 보이지 않는 틈이나 겹침이 생깁니다.

이번에도 터지지 않았습니다. 유일한 비거래일 앵커였던 6월 14일이 두 tolerance 모두에서 같은 날(6월 15일)로 수렴했고, 게다가 그건 마지막 앵커라 시작점 역할로는 쓰이지도 않았습니다. 두 개의 우연이 겹쳐서 살았습니다. 조건이 조금만 달랐다면 구간 길이가 소리 없이 어긋났을 겁니다.

세 사고의 공통점

셋을 나란히 놓으면 한 가지가 보입니다.

세 사고의 구조
사고뿌리결과
연휴 유령 날짜비거래일에 라벨만 남김터짐
일요일 앵커비거래일을 거래일로 스냅운으로 안전
tolerance 불일치같은 날짜의 두 역할운으로 안전

셋 다 뿌리가 같습니다. 달력의 날짜와 거래일을 같은 것으로 취급한 것. 하지만 이 둘은 다릅니다. 달력에는 매일 날짜가 있지만, 시장은 그중 일부에서만 열립니다. "6월 14일"이라는 문자열이 반드시 "그날 형성된 가격"을 뜻하지는 않습니다.

그리고 이 계열의 사고가 위험한 진짜 이유는, 이전 글(LAG 함수가 조용히 틀리는 이유)에서 본 것과 정확히 같습니다 — 데이터에 따라 터지기도 하고 안 터지기도 합니다. 갭이 없는 날엔 정상이고, 연휴가 끼는 날엔 틀립니다. 대부분의 날에 정상으로 보이는 코드가 특정 날짜에만 조용히 어긋납니다. 그리고 그 어긋난 값도 완벽하게 정상적으로 생겼습니다.

남는 교훈

1. 거래일 달력을 유일한 진실로 삼으십시오. "어제"도, "한 달 전"도, "가장 가까운 날"도 전부 거래일 달력에서 조회해야 합니다. 날짜 뺄셈으로 구한 날은 시장이 열렸다는 보장이 없습니다.

2. 값과 라벨을 함께 옮기십시오. 연휴 사고는 값을 10월 2일 것으로 채우면서 라벨만 10월 6일로 남겨 터졌습니다. 데이터를 다른 날짜로 채울 때는 "이 값이 실제로 언제 것인지"를 라벨이 정직하게 반영해야 합니다. 채운 값이라면 채웠다고 표시하는 편이 낫습니다.

3. 같은 규칙을 두 곳에 쓰면 두 곳이 같은지 확인하십시오. tolerance 사고는 시작점과 끝점에 다른 규칙을 준 데서 나왔습니다. 하나의 대상을 두 문맥에서 다루면서 규칙이 갈라지면, 그 둘이 만나는 지점에서 불일치가 샙니다.

4. 안 터진 버그를 운으로 넘기지 마십시오. 사고 2와 3은 결과적으로 무사했습니다. 하지만 "이번엔 괜찮았다"와 "구조가 안전하다"는 다른 말입니다. 운으로 살아남은 코드는 조건이 바뀌는 순간 터집니다. 앵커를 더 촘촘히 잡거나 다른 기간에 재사용하면, 지금 잠자는 이 결함들이 깨어납니다.

이 세 사고 중 실제로 데이터를 틀리게 만든 건 하나뿐이었고, 그것도 발행주식수 역산이라는 우연한 교차검증으로 잡았습니다. 나머지 둘은 아직 터지지 않았습니다. 이 글은 그 둘이 언젠가 터질 것이라는 예고이자, 그때 당황하지 않기 위한 기록입니다.

이 글에 관하여 — 여기 적은 세 사고는 FomoBot의 데이터를 분석하고 검증하는 과정에서 실제로 드러난 것입니다. 사고 1(연휴 라벨)은 개별 스냅샷 조회 시점에 발견돼 해당 사례를 관련 분석에서 제외했고, 사고 2·3(앵커 스냅)은 결과에 영향이 없음을 확인한 뒤 구조적 개선 과제로 남겼습니다. 구체적인 배치 구성이나 내부 시스템 세부는 이 글의 논지와 무관해 생략했습니다.

투자 판단에 대하여 — FomoBot은 이미 지나간 가격을 정리해 보여주는 도구입니다. 이 글은 개발 기록이며, 어떤 내용도 투자 자문이 아닙니다.