Forge Fate
퀀트·데이터 연구

그 데이터는 그때도 알 수 있었을까? AI 투자 실험 전에 확인할 데이터의 함정

약 24분

근거와 검증 범위 — 인공 데이터의 시간 정렬 예제

문헌과 인공 가격 수열로 데이터 시점과 신호 지연을 설명합니다. +21%·−19%는 예제 수열의 계산값이며 실제 전략 성과가 아닙니다. 거래비용과 실제 체결 가능성은 이 예제로 검증하지 않습니다.

수정주가·상장폐지·공시일·빈티지·시간대를 점검하고, 종가 신호를 한 칸 지연해 보는 백테스트 데이터 감사 입문

1편에서는 AI를 자동 종목 추천기가 아니라 투자 아이디어의 검증을 돕는 도구로 보았다. 2편에서는 유행하는 도구보다 문제와 입력, 출력, 평가 기준을 먼저 정해야 한다고 결론 내렸다.

그다음에는 어떤 모델을 선택해야 할까?

이번 편에서는 모델 선택을 잠시 미룬다. 문제를 잘 정의하고 평가 기준을 정했더라도, 입력 데이터에 당시 알 수 없었던 값이 들어가 있다면 실험의 시간 순서가 이미 무너졌기 때문이다.

처음 가격 데이터를 내려받았을 때 나는 결측치가 없는 CSV를 보면 안심했다. 코드가 오류 없이 실행되고 백테스트 수익률까지 높게 나오면 데이터도 전략도 꽤 괜찮다고 생각했다.

하지만 close와 adjusted_close의 차이, 사라진 종목, 재무제표의 실제 공시일, 경제지표의 수정 이력, 거래소 시간대와 신호 지연을 확인하기 시작하면서 판단이 달라졌다. 기업행동에는 발표일·권리락일·기준일·지급일처럼 서로 다른 날짜가 존재하고[S1], 조정 가격의 제공 방식은 데이터 공급자의 상품과 설정에 따라 달라질 수 있다.[S4] 현재 살아남은 종목만 과거에 사용하면 상장폐지 종목의 기록이 빠질 수 있으며[S6], 회계기간 말일과 재무정보의 실제 제출일도 같지 않다.[S7]

그래서 데이터 감사의 첫 질문은 “빈칸이 있는가?”가 아니게 됐다.

백테스트에 들어간 이 데이터는 그 거래 결정을 내리던 순간에도 실제로 알 수 있었는가?

이 글의 표와 코드는 데이터 정렬을 설명하기 위한 교육·연구용 예제다. 특정 종목 추천이나 실제 매매 지시가 아니며, 수익을 보장하지 않는다.

데이터 한 행에는 여러 개의 시간이 숨어 있다

일별 데이터에는 흔히 date라는 열 하나만 있다. 그러나 하나의 값이 투자 결정에 사용되기까지는 여러 시간이 존재한다.

text
사실이 측정된 기간의 끝period_end    ↓처음 공개·제출된 시각release_at / filed_at    ↓공급자를 통해 입수 가능해진 시각vendor_available_at    ↓신호를 확정한 시각decision_at    ↓주문 제출과 체결을 가정한 시각execution_at

이 시각들은 같은 날짜로 표시될 수 있어도 같은 순간을 뜻하지 않는다. 데이터가 어느 기간을 설명하는지, 언제 처음 공개됐는지, 연구자가 언제 받을 수 있었는지, 신호와 주문이 언제 만들어졌는지를 구분해야 한다.

백테스트에 값을 사용할 수 있는지를 판단하는 운영 규칙은 다음처럼 표현할 수 있다.

text
vendor_available_at <= decision_at < execution_at

다만 실제 적용 조건은 데이터 공급 경로, 시장 세션과 주문 규칙에 따라 달라진다. 공급자의 전달 시각을 확인할 수 없다면 이를 임의로 채우지 말고 명시적인 지연 가정이나 연구 공백으로 기록해야 한다.

한 행의 date만 맞추는 것으로는 충분하지 않다. 핵심은 각 값이 생성되고 공개되고 관측된 뒤에야 의사결정과 실행에 사용됐는지를 추적하는 것이다.

close와 adjusted_close는 같은 가격이 아니다

주식분할이 만든 기계적 가격 단절

2대1 주식분할은 주식 수를 두 배로 늘리고 주당 가격을 절반으로 바꾸지만, 분할 자체가 주주의 보유 지분가치를 절반으로 줄이는 것은 아니다.[S2]

시점보유 주식 수원시 주가보유 지분가치
분할 전1주100100
2대1 분할 후2주50100

분할 전 원시 종가 100과 분할 후 원시 종가 50만 비교하면 수익률은 -50%다. 그러나 이 값은 경제적 손실이 아니라 기업행동으로 발생한 기계적 가격 단절일 수 있다.[S2]

직접 확인된 범위는 여기까지다. 이동평균·변동성 같은 특징의 변화, 이상치 규칙의 오작동, 모델의 잘못된 패턴 학습은 이러한 단절에서 이어질 수 있는 후속 영향이자 점검 가설이다. 실제로 발생했는지는 각 파이프라인에서 따로 검증해야 한다.

  • 분할일 전후의 특징값이 비정상적으로 끊기는가?
  • 이상치 제거 규칙이 기업행동이 있었던 행을 삭제하는가?
  • 조정 전후에 모델 입력과 결과가 크게 달라지는가?
  • 원시 가격의 기계적 단절이 예측 신호처럼 사용되는가?

따라서 큰 가격 변화가 발견되면 실제 급락으로 단정하기 전에 분할, 배당, 분사, 권리공모와 통화·단위 오류를 확인해야 한다.

배당은 분할과 같은 조정이 아니다

분할과 배당은 모두 가격 시계열에 영향을 줄 수 있지만 같은 사건은 아니다. 수익률의 정의도 연구 목적에 따라 달라진다.

  • 가격수익률은 가격 변화에 초점을 둔다.
  • 총수익률은 배당 재투자를 포함한다.
  • 순총수익률은 원천징수세 같은 추가 가정을 반영할 수 있다.[S3]

따라서 “이 전략의 수익률”이라고만 쓰면 무엇을 측정했는지 불분명하다. 순수한 가격 변화를 연구하는지, 배당을 재투자한 보유 성과를 연구하는지에 따라 적절한 열이 달라진다.

점검 대상확인할 열·정보
가격 기준raw_close, adjusted_close
수익률 기준price_return_index, total_return_index, net_total_return_index
분할action_type, split_ratio
배당dividend_type, dividend_amount
통화·세금가격·배당 통화, 환율 기준, 세금 가정

가격수익률과 총수익률은 서로 다른 측정치이므로 같은 기준인 것처럼 비교해서는 안 된다.[S3]

수정주가라는 이름만 믿지 않는다

adjusted_close라는 열 이름만 보고 모든 문제가 해결됐다고 생각하기 쉽다. 그러나 수정주가는 모든 공급자가 같은 방식으로 정의하는 보편적인 열이 아니다.

예를 들어 Alpha Vantage는 원시 일별 OHLCV와 별도로 분할·배당을 반영한 조정 시계열을 문서화하며, 장중 자료에서는 조정 여부와 정규장·연장시간 포함 여부를 각각 설정할 수 있다.[S4] 이것은 한 공급자의 사례일 뿐이며 다른 공급자의 조정 범위와 공식이 같다는 뜻은 아니다.

공급자별 문서에서 다음 사항을 확인해야 한다.

  • 분할만 조정하는지
  • 현금배당과 특별배당을 어떻게 처리하는지
  • 분사나 권리공모를 반영하는지
  • 과거 가격을 어느 방향으로 소급 조정하는지
  • 거래량도 함께 조정하는지
  • 정규장 외 거래를 포함하는지
  • 과거값 정정이 언제 반영되는지

기업행동 데이터에는 선언일, 발표일, 권리락일, 기준일, 지급일과 최종 갱신일처럼 의미가 다른 날짜 필드가 존재할 수 있다.[S1] 이를 하나의 date로 축약하면 사건이 알려진 시점과 권리가 적용된 시점, 데이터가 갱신된 시점이 섞인다.

실험 기록에는 다음 정보를 남긴다.

  • 데이터 공급자와 상품·엔드포인트
  • 가격 열의 정확한 이름
  • 반영된 기업행동의 범위
  • 조정 방향과 계산 규칙
  • 정규장·연장시간 포함 설정
  • API 또는 파일 버전
  • 다운로드와 재다운로드 시각
  • 원본 파일의 체크섬

확인한 공개 자료만으로는 모든 공급자의 조정 공식과 과거값 수정 정책을 판정할 수 없다. 따라서 “수정주가를 사용했다”는 한 문장 대신 어느 공급자의 어떤 정의를 사용했는지 기록해야 한다.

결측치와 이상치: 지우기 전에 발생 이유부터 묻는다

같은 NaN도 의미는 다르다

시계열에서 NaN은 원인이 아니라 관측된 상태다. 같은 빈칸이라도 다음처럼 다른 사건과 연결될 수 있다.

  • 시장 휴장이나 자산별 거래일 불일치
  • 거래정지 또는 유동성 부족
  • 신규 상장 전이나 상장폐지 후의 기간
  • 공급 지연이나 수집 실패
  • 종목 식별자 변경 또는 조인 실패
  • 공시되지 않았거나 적용되지 않는 재무 항목

이 목록만 보고 개별 결측의 원인을 단정할 수는 없다. 거래 달력, 상장 상태, 공급 로그와 원자료를 함께 확인해야 한다.

결측행을 모두 삭제하는 것도 중립적인 청소가 아니다. 특정 변수가 존재하는 완전 관측치만 남기면 원래 표본의 비무작위 부분집합이 만들어지고, 수익률 분포나 변수 사이의 관계가 달라질 수 있다는 실증 연구가 있다.[S5] 다만 이 결과가 모든 데이터에서 편향의 방향과 크기를 미리 정해 주지는 않는다.[S5]

이 행은 왜 비었으며, 이 행을 제거하면 어떤 종목과 시장 상태가 표본에서 사라지는가?

전일 값 채우기도 하나의 가정이다

ffill()은 단순히 빈칸을 없애는 명령이 아니다. 이전 값이 다음 시점에도 계속 유효하다는 가정을 추가한다.

그 가정이 적절한지는 열의 의미와 데이터 달력에 따라 별도로 확인해야 한다. 예를 들어 연구 규칙상 재무정보를 다음 공시까지 유지할 수는 있지만, 같은 처리를 가격이나 거래량에 적용해도 되는지는 자동으로 결정되지 않는다. 휴장, 거래정지, 거래일 불일치 또는 조인 실패가 포함된 데이터에서는 채우기 전후의 행 의미와 수익률 계산이 어떻게 달라지는지 검증해야 한다.

서로 다른 시장의 자료를 결합할 때도 달력과 마감 시각을 확인해야 한다. 한 시장이 휴장한 날짜에 이전 값을 유지하는 것이 연구상 유효한 상태 표현인지, 단지 관측되지 않은 값을 생성하는 것인지는 데이터 정의만으로 일률적으로 답할 수 없다.

모든 결측에 적용할 하나의 최선 처리법은 없다. 원인과 연구 목적에 따라 삭제·유지·대체 규칙을 나누고 다음 내용을 기록한다.

  • 대체 대상 열의 의미
  • 이전 값이 계속 유효하다고 본 근거
  • 사용한 거래 달력과 조인 규칙
  • 대체 전후의 관측치 수
  • 수익률·특징·표본 구성의 변화
  • 확인하지 못한 한계

이상치는 오류일 수도, 중요한 사건일 수도 있다

관측우선 확인할 후보무조건 삭제할 때의 위험
하루 동안 큰 가격 하락분할·배당·통화 오류·실제 하락기업행동 또는 실제 극단 사건 제거
거래량 급증단위 오류·분할 조정·실제 거래 증가사건일 정보 제거
가격 또는 거래량 0거래정지·결측 대체·공급자 표기서로 다른 상태를 동일하게 처리
중복 타임스탬프여러 거래소·수정 레코드·중복 수집시점과 가격 정보의 임의 손실

큰 가격 단절은 분할 같은 기업행동 때문에 발생할 수 있다.[S1][S2] 그렇다고 모든 큰 변동이 기업행동이라는 뜻도 아니다. 원자료와 사건 데이터를 대조해 원인을 분류해야 한다.

결측·이상치 처리 기록에는 원본 값, 탐지 규칙, 원인 분류, 확인 자료, 수정 전후 값, 영향을 받은 행의 수와 처리 이유를 남긴다. 처리 전후 결과도 비교해 원본에서 최종 데이터까지의 변화를 재현할 수 있어야 한다.

현재 살아남은 종목만 보면 과거가 달라진다

종목 목록도 시점별 데이터다

현재 존재하는 종목 목록을 과거 전체 기간에 그대로 적용한다고 해보자.

text
당시 실제 유니버스├─ 이후에도 생존한 종목├─ 합병·인수된 종목├─ 상장폐지된 종목└─ 파산·거래정지 등을 겪은 종목현재 목록으로 재구성한 과거 유니버스└─ 이후에도 생존한 종목 중심

이 방법에서는 이후 사라진 종목이 과거에도 없었던 것처럼 처리된다. 미래에 살아남았다는 결과가 과거의 종목 선택 조건에 들어간 것이다.

따라서 백테스트의 종목 유니버스도 고정된 목록이 아니라 시점별 데이터로 다뤄야 한다. 각 리밸런싱 시점에 실제로 상장돼 있었고 연구 조건에 따라 선택 가능했던 종목을 복원해야 한다.

상장폐지 직전까지만 계산하면 마지막 수익률이 빠질 수 있다

현재 종목 목록을 사용하는 문제와 상장폐지 수익률 누락은 구분할 필요가 있다.

어떤 종목의 마지막 정상 거래일까지 수익률을 계산한 뒤 행을 제거하면 상장폐지 과정에서 발생한 수익률이나 지급 대가가 빠질 수 있다. Shumway의 원 연구는 역사적 CRSP 자료에서 부정적 사유의 상장폐지 수익률이 다수 누락돼 있었으며, 그 누락이 큰 편향을 만들 수 있음을 보였다.[S6]

다만 이 연구는 1962년 이후의 미국 CRSP 자료를 분석한 역사적 결과다.[S6] 현재의 모든 공급자와 시장에 같은 누락이 있다고 일반화할 수 없다. 2026년 현재 상업 데이터베이스별 상장폐지 자료의 완전성도 공개 자료만으로 확정하지 못했다.

확인해야 할 항목은 다음과 같다.

  • 영구 종목 식별자와 당시 티커
  • list_date, delist_date, delist_reason
  • 유니버스 편입일과 퇴출일
  • 당시 지수 또는 시장 구성 여부
  • 마지막 거래가격과 상장폐지 수익률
  • 현금·주식 지급 대가
  • 합병·인수·파산·거래정지 상태

실험 기록에는 시점별 유니버스 생성 절차, 상장폐지 종목의 포함 여부, 마지막 수익률과 지급 대가의 처리 규칙을 남긴다. 정보가 제공되지 않는다면 임의의 정상 수익률로 채우지 말고 누락 사실과 대체 가정을 밝혀야 한다.

살아남은 종목만으로 만든 과거는 당시 투자자가 마주했던 시장과 다르다.

Look-ahead bias를 이 글에서 판정하는 기준

이 글에서는 결정 시각 뒤에야 이용할 수 있었던 값을 그 결정에 사용한 경우를 운영상 look-ahead bias로 판정한다. 이는 미래 수익률 열을 모델 입력에 직접 넣는 경우에만 한정되지 않는다.

대표적인 점검 대상은 다음과 같다.

  • 오늘 종가로 신호를 계산하고 오늘의 종가수익률에 적용한다.
  • 분기말 재무값을 분기말부터 사용할 수 있었다고 가정한다.
  • 나중에 수정된 경제·재무 값을 최초 발표 때부터 사용한다.
  • 현재 종목 목록을 과거 유니버스로 사용한다.
  • 날짜만 맞추고 실제 공개 시각과 시장 마감을 무시한다.

정규화나 결측 대체 과정에서도 시간 순서 위반이 생길 가능성은 점검할 수 있지만, 이번 Research에는 특정 처리법이 실제로 미래값을 포함했다는 직접 근거가 없다. 따라서 개별 파이프라인의 계산 구간과 입력 행을 확인하기 전에는 해당 오류가 존재한다고 단정하지 않는다.

높은 백테스트 성과는 look-ahead bias의 존재를 입증하지도, 시간 정렬의 정확성을 보장하지도 않는다. 판정하려면 특징별 이용 가능 시각, 조인 기준, 같은 날짜 안의 적용 순서, 신호 지연 규칙과 실행가격 정의를 추적해야 한다.

이 값이 측정된 날짜가 아니라, 내가 실제로 받을 수 있었던 가장 이른 시각은 언제인가?

회계기간 말일에는 아직 재무제표를 알 수 없다

다음과 같은 분기 재무정보가 있다고 하자.

text
회계기간 종료: 2026-06-30실제 공시: 그 이후의 특정 날짜와 시각

2026-06-30은 실적이 측정된 기간의 끝이다. 완성된 재무제표가 시장에 알려진 시점은 아니다. SEC EDGAR API는 공시가 전파될 때 제출 자료와 XBRL 데이터를 갱신하며, 대상 회계기간과 제출 관련 정보를 별도로 제공한다.[S7]

따라서 period_end를 기준으로 재무값을 가격 데이터에 즉시 결합하면 미래 정보를 앞당겨 사용할 수 있다. 실제 filed_at이나 확인 가능한 공개 시각 이후, 연구에서 정의한 첫 거래 가능 시점부터 해당 값을 사용해야 한다.

확인할 메타데이터는 다음과 같다.

  • fiscal_period_start, fiscal_period_end
  • filed_at, accepted_at
  • 공시 유형과 수정 공시 여부
  • accession number 같은 영구 문서 식별자
  • 동일 기간의 최초값과 수정값
  • 값의 유효 시작·종료 시점

공시가 장 마감 뒤에 나왔다면 같은 거래일 종가를 결정할 때 사용할 수 없다. 다만 SEC 제출 시각만으로 개별 공급자의 전달 지연과 사용자의 처리 완료 시각까지 확정할 수는 없다.[S7] 이 정보가 없다면 보수적인 적용 시점이나 명시적인 지연 규칙을 사용하고 그 한계를 기록해야 한다.

오늘 보이는 과거값은 당시 발표된 값이 아닐 수 있다

경제지표에는 빈티지가 있다

오늘 데이터베이스에서 조회한 과거 경제지표가 당시 발표됐던 값과 같다고 가정해서는 안 된다.

FRED의 기본 관점은 현재 이용 가능한 값을 보여 준다. 과거 특정 시점에 알려졌던 값을 요청하려면 ALFRED의 실시간 기간 정보를 활용할 수 있다.[S8] FRED의 빈티지 날짜는 계열 값이 수정되거나 새 값이 공개된 날짜를 추적하는 데 사용된다.[S9]

text
관측 대상 기간≠ 최초 발표일≠ 수정 또는 새 값이 공개된 빈티지 날짜

현재의 최신값으로 과거 신호를 다시 만들면 당시에는 존재하지 않았던 수정 정보가 들어갈 수 있다.

GDP는 수정 이력의 중요성을 보여 준다

미국 분기 GDP는 속보·2차·3차 추정치로 발표되며, 후속 추정치는 더 상세하고 포괄적인 자료를 반영한다. 이후 연례·종합 개정도 이루어진다.[S10]

따라서 오늘 조회한 과거 GDP 값은 당시 최초 발표값과 다를 수 있다. 최신 GDP 계열을 과거 시점부터 알고 있었다고 처리하면 사후 정보를 사용하게 된다.

이는 미국 GDP의 사례이며 다른 국가와 지표가 같은 발표·개정 구조를 가진다는 뜻은 아니다. 재무자료에도 수정 공시가 있을 수 있으므로 최초 공개값과 후속값을 구분해 보존해야 한다.[S7]

확인할 메타데이터는 다음과 같다.

  • 관측 대상일 또는 기간
  • 최초 발표일과 발표 시각
  • 빈티지 날짜
  • realtime_start, realtime_end
  • 수정 차수와 각 시점의 값
  • 계절조정·기준연도·정의 변경
  • 데이터 스냅숏 시각

ALFRED가 모든 국가·지표·민간 자료의 최초 발표본을 포괄하는 것은 아니다.[S8][S9] 당시 빈티지를 확보하지 못했다면 최신값을 당시값처럼 표현하지 말고, 그 제한 때문에 연구 질문이 어떻게 좁아졌는지 밝혀야 한다.

날짜가 같아도 시장의 시각은 다르다

NYSE의 핵심 거래시간은 미국 동부시간 09:30~16:00이고 종가 경매는 16:00에 열린다.[S11] 따라서 NYSE의 당일 공식 종가로 만든 신호를 별도의 실행 가정 없이 같은 날 그 종가에 이미 체결했다고 처리하면 정보와 실행의 순서가 뒤집힌다.

2026-08-31이라는 날짜 하나만으로는 다음을 알 수 없다.

  • 현지시간인지 UTC인지
  • 정규장 봉인지 연장시간을 포함한 봉인지
  • 공식 종가인지 마지막 일반 체결인지
  • 일광절약시간이 적용됐는지
  • 조기 폐장일인지
  • 공급자가 값을 언제 전달했는지

NYSE 자료는 NYSE 및 관련 시장의 현재 거래시간만 뒷받침한다.[S11] 다른 거래소·자산·세션의 규칙이나 조기 폐장, 공급 지연까지 설명하는 보편 규칙은 아니다. 각 시장의 거래 세션과 달력을 별도로 확인해야 한다.

이 글에서는 시간 감사를 위해 다음 열을 확인한다.

  • 시간대가 포함된 event_at, received_at
  • bar_start, bar_end
  • 거래소와 거래 세션
  • 공식 종가 여부
  • 현지 거래일과 UTC 변환값
  • 신호 계산 완료 시각
  • 주문 제출·체결 시각
  • 거래 달력 버전과 조기 폐장 표시

원본의 시간대 정보와 현지 거래일을 보존하고 필요할 때 UTC 변환값을 함께 두는 것은 이 글에서 제안하는 감사 방법이다. 이것만으로 모든 시장의 정렬이 안전해지는 것은 아니다. 날짜 문자열로 조인한 뒤에는 시장별 세션과 마감 순서를 대조해 한쪽의 나중 정보를 다른 쪽의 이른 결정에 붙이지 않았는지 직접 검사해야 한다.

날짜를 맞춘 것은 시간을 맞춘 것이 아니다.

작은 재현 실험: 종가 신호를 같은 날 수익률에 적용한 오류

아래 가격 수열은 시간 정렬만 설명하기 위해 만든 인공 데이터다. 계산된 수익률과 누적값은 실제 전략 성과가 아니며 다른 자산·시장·기간으로 일반화할 수 없다.

신호 규칙은 의도적으로 단순하다.

오늘 종가가 전일 종가보다 높으면 1, 아니면 0.

인공 데이터

날짜종가당일 수익률당일 종가 신호
2026-01-05100.00결측0
2026-01-06110.00+10%1
2026-01-0799.00-10%0
2026-01-08108.90+10%1
2026-01-0998.01-10%0

pct_change()는 현재 행과 이전 행 사이의 상대 변화를 계산한다.[S12] 따라서 1월 6일의 +10%는 1월 5일 종가부터 1월 6일 종가까지 이미 발생한 변화다.

잘못된 백테스트

python
df["return"] = df["close"].pct_change()df["signal_at_close"] = (    df["close"] > df["close"].shift(1)).astype(int)df["wrong_return"] = (    df["signal_at_close"] * df["return"])

signal_at_close는 오늘 종가가 확정돼야 계산할 수 있다. 그런데 같은 행의 return은 전일 종가부터 오늘 종가까지의 수익률이다.

두 값을 같은 행에서 곱하면 오늘 상승을 확인한 뒤, 그 상승이 시작되기 전부터 신호를 보유했던 것처럼 계산한다. 같은 행에 있다는 사실은 같은 시점에 이용 가능했다는 뜻이 아니다.

신호를 한 칸 지연한다

python
df["delayed_signal"] = (    df["signal_at_close"].shift(1))df["aligned_return"] = (    df["delayed_signal"] * df["return"])

shift(1)을 사용하면 이전 행에서 계산된 신호를 현재 행의 수익률에 정렬할 수 있다.[S12]

날짜종가당일 수익률당일 종가 신호잘못 적용한 수익률1칸 지연 신호정렬한 수익률
2026-01-05100.00결측0결측결측결측
2026-01-06110.00+10%1+10%00%
2026-01-0799.00-10%00%1-10%
2026-01-08108.90+10%1+10%00%
2026-01-0998.01-10%00%1-10%

잘못 정렬한 인공 누적값은 다음과 같다.

text
(1.10 × 1.10) - 1 = +21%

한 칸 지연한 인공 누적값은 다음과 같다.

text
(0.90 × 0.90) - 1 = -19%

두 값의 차이는 이 신호가 실제 시장에서 수익을 낼 수 있는지 보여 주지 않는다. 상승과 하락이 번갈아 나타나도록 만든 수열이므로 +21%와 -19% 모두 실제 전략 성과로 일반화할 수 없다.

중요한 것은 수익률의 크기가 아니다. 신호가 완성된 뒤의 기간에만 그 신호를 적용했는가가 판정 기준이다.

직접 실행할 수 있는 전체 코드

python
import pandas as pddf = pd.DataFrame(    {"close": [100.00, 110.00, 99.00, 108.90, 98.01]},    index=pd.date_range("2026-01-05", periods=5, freq="B"),)df["return"] = df["close"].pct_change()# 오늘 종가가 전일 종가보다 높으면 1df["signal_at_close"] = (    df["close"] > df["close"].shift(1)).astype(int)# 오류: 오늘 종가로 만든 신호를# 이미 발생한 오늘의 종가수익률에 적용df["wrong_return"] = (    df["signal_at_close"] * df["return"])# 최소 시점 교정: 전 거래일까지 계산된 신호를# 다음 행의 수익률에 적용df["delayed_signal"] = (    df["signal_at_close"].shift(1))df["aligned_return"] = (    df["delayed_signal"] * df["return"])print(df.round(4))cumulative = (    1    + df[["wrong_return", "aligned_return"]].fillna(0)).prod() - 1print(cumulative)

shift(1)은 당일 종가로 만든 신호를 이미 발생한 당일 수익률에 적용하는 오류를 제거하는 최소한의 교정이다. 다음 날 시가나 다른 가격에서 실제로 체결됐음을 보장하지는 않는다.

실제 연구에서는 봉 종료, 신호 계산 완료, 주문 제출과 체결 가정, 거래정지·미체결 및 조기 폐장 규칙을 추가로 정해야 한다. 거래비용과 위험 지표는 5편의 범위이므로 이 예제에는 포함하지 않는다.

한 칸 이동의 목적은 성과를 개선하는 것이 아니라 정보 시점과 실행 시점의 인과 순서를 지키는 것이다.

구하기 쉬운 데이터만 고르면 질문 자체가 바뀐다

데이터를 구하기 쉽다는 이유로 표본을 정하는 것도 연구 결과에 영향을 줄 수 있다.

  • 긴 가격 이력이 있는 자산만 선택한다.
  • API가 편리한 미국 대형주만 선택한다.
  • 결측 없는 재무항목을 가진 기업만 선택한다.
  • 특정 시장 국면만 들어 있는 기간을 고른다.
  • 무료 자료가 있는 시장만 연구한다.
  • 현재 인기 있는 자산군을 과거 표본으로 선택한다.

이런 선택이 항상 잘못은 아니다. 미국 대형주나 특정 기간만 분석하는 것은 명확한 연구 범위가 될 수 있다.

문제는 데이터 가용성 때문에 제한된 표본을 전체 시장이나 다른 기간에도 적용되는 결론처럼 서술하는 것이다. 완전 관측치만 요구해도 비무작위 표본이 만들어질 수 있다는 실증 근거가 있다.[S5] 편향의 방향과 크기는 선택 규칙과 누락 구조에 따라 달라질 수 있으므로 성과를 언제나 높인다고 단정해서도 안 된다.[S5]

실험 기록에는 다음 내용을 남긴다.

  • 설명하려는 목표 모집단
  • 실제 사용한 표본
  • 시장·자산·기간 범위
  • 포함·제외 규칙
  • 제외 이유별 종목과 관측치 수
  • 데이터 가용성 때문에 달라진 연구 질문
  • 결론을 일반화할 수 있는 범위

편리하게 구한 표본과 내가 설명하려던 전체 시장이 같은지는 별도의 검증 질문이다.

나의 판단 변화: 깨끗한 CSV에서 데이터 계보로

구분지금까지의 판단
이전 생각결측치가 없는 CSV이고 백테스트 수익률이 높으면 데이터가 충분히 깨끗하다고 생각했다.
이번에 확인한 것원시 가격과 수정주가는 같지 않으며, 분할과 배당은 수익률 계산에서 서로 다른 의미를 갖는다.[S2][S3][S4] 기업행동에는 여러 날짜가 존재하고[S1], 결측행 삭제도 표본을 바꿀 수 있다.[S5] 상장폐지 종목과 마지막 수익률이 빠지면 과거 시장의 모습도 달라질 수 있다.[S6]
현재 판단공시 제출일, 데이터 빈티지, 종목 편입·퇴출, 시간대와 신호 지연을 확인하지 않은 결과는 모델이 아무리 복잡해도 신뢰하기 어렵다.[S6][S7][S8][S9][S10][S11][S12]
아직 모르는 것공급자별 모든 조정 공식, 현재 상업 데이터의 상장폐지 자료 완전성, 공급자 전달 지연과 실제 체결 가능성은 확보한 공개 자료만으로 확정하지 못했다.

공부하면서 바뀐 것은 모델에 대한 취향보다 결과를 믿기 위해 요구하는 기록의 수준이었다.

예전에는 데이터 파일을 모델의 입력으로만 봤다. 지금은 그 값이 어디에서 왔고, 언제 생성됐고, 언제 받을 수 있었으며, 어떤 행이 수정·대체·제외됐는지를 함께 본다. 알 수 없는 부분은 그럴듯하게 채우기보다 보수적인 가정이나 연구 범위의 한계로 남긴다.

내 데이터셋에 작성할 감사표

다음 표는 결과가 나쁜 이유를 찾는 사후 점검표가 아니다. 실험을 시작하기 전에 작성할 설계 문서에 가깝다.

감사 질문반드시 확인할 열·자료실험 기록에 남길 것
가격은 무엇을 뜻하는가?원시·수정 종가, 가격·총수익 지수선택한 열과 선택 이유
무엇이 조정됐는가?분할, 배당, 분사, 권리공모, 조정계수공급자 정의, 버전, 재다운로드 시각
기업행동은 언제 알려졌는가?선언·발표·권리락·기준·지급·갱신일신호에 사용한 이용 가능일
결측은 왜 생겼는가?거래 달력, 거래정지, 상장·상폐 상태, 조인 로그원인별 건수와 처리 규칙
이상치는 실제 사건인가?기업행동, 통화·단위, 거래량, 중복 레코드원본·수정값과 판단 근거
당시의 종목 유니버스인가?편입·퇴출일, 상폐일·사유, 영구 ID시점별 유니버스 생성 절차
재무값은 언제 공개됐는가?기간 말일, 제출·접수 시각, 공시 ID최초값과 수정값의 유효기간
경제값은 어느 빈티지인가?발표일, 빈티지 날짜, 수정 차수당시값 조회 조건
시간대가 맞는가?원본 시간대, 현지 거래일, 거래소, 세션변환 규칙과 시장별 거래 달력
신호 뒤에 체결했는가?봉 종료, 계산 완료, 주문·체결 시각shift와 실행가격 규칙
표본은 왜 이 범위인가?목표 모집단, 시장·자산·기간, 제외 이유제외 흐름과 일반화 한계
결과를 재현할 수 있는가?코드, 환경, 원본 파일과 체크섬실행일, 라이브러리 버전, 설정

자신의 데이터에서는 다음 순서로 시작할 수 있다.

  1. close와 adjusted_close의 정의를 공급자 문서에서 확인한다.[S4]
  2. 분할·배당 자료에 어떤 날짜 열이 존재하는지 기록한다.[S1]
  3. 결측치를 원인별로 집계하고 현재 삭제·대체 규칙을 적는다.[S5]
  4. 시점별 유니버스와 상장폐지 종목이 포함됐는지 확인한다.[S6]
  5. 재무자료가 period_end가 아니라 실제 공시 이후부터 적용됐는지 확인한다.[S7]
  6. 경제지표가 최신 수정값인지 당시 빈티지인지 확인한다.[S8][S9][S10]
  7. 시장별 타임스탬프, 거래 세션과 마감 순서를 기록한다.[S11]
  8. 기존 신호를 shift(1) 한 결과와 비교한다.[S12]

마지막 비교에서 결과 차이가 작더라도 그것이 시점 오류가 없다는 뜻은 아니다. 결과의 크기가 아니라 정보가 완성된 뒤에만 신호를 적용했는지를 확인해야 한다.

이번 편에서 결론 내리지 않을 것

데이터의 시간 순서를 맞추는 것만으로 공정한 백테스트가 완성되지는 않는다.

4편에서는 학습·검증·테스트 구간 분리, 랜덤 분할과 금융 시계열 분할의 차이, walk-forward validation, 데이터 스누핑, 다중 검정, 파라미터 탐색과 테스트 구간 오염을 별도로 다룬다. 이번 편에서는 입력 데이터의 시계가 맞아야 그다음 검증 절차도 의미가 있다는 연결점까지만 남긴다.

수수료, 스프레드, 시장충격, 세금과 위험 지표는 5편의 범위다. 이 글의 인공 예제에는 그런 요소가 없으므로 전략 성과로 해석할 수 없다.

LLM의 안전한 활용 절차와 프롬프트 설계는 10·11편에서 다룬다.

결론: 성능표보다 먼저 데이터의 이력을 본다

원시 가격과 수정주가를 구분하고[S2][S4], 가격수익률과 총수익률을 구분해야 한다.[S3] 기업행동의 여러 날짜와 수정 이력을 보존하고[S1], 결측·이상 관측치는 발생 원인과 처리 가정을 확인해야 한다.[S5]

현재 살아남은 종목만 과거로 가져오지 말고 시점별 유니버스와 상장폐지를 추적해야 한다.[S6] 재무자료는 회계기간 말일이 아니라 실제 공개 이후부터 사용하고[S7], 경제지표는 현재 최신값과 당시 빈티지를 구분해야 한다.[S8][S9][S10]

날짜뿐 아니라 시장별 시간대와 장 마감, 신호 계산과 실행의 순서도 맞춰야 한다.[S11][S12] 구하기 쉬운 표본이 목표 모집단을 대신하고 있지는 않은지도 기록해야 한다.[S5]

확인할 수 없는 공급자 지연, 조정 규칙과 자료 완전성은 추측으로 채우지 않는다. 가정과 연구 공백으로 남긴다.

모델 성능을 보기 전에 각 데이터가 언제 생성되고 언제 관측 가능했으며, 어떤 방식으로 수정·대체·제외되었는지를 추적해야 한다.

결측치 없는 CSV와 높은 백테스트 수익률은 이 검사를 대신하지 못한다.

이 글의 표와 코드는 데이터 정렬을 설명하기 위한 교육·연구용 예제다. 특정 종목 추천, 실제 매매 지시 또는 수익 보장을 제공하지 않으며, 인공 데이터의 계산 결과를 실제 전략 성과로 일반화해서는 안 된다.

데이터의 시계를 맞췄다면 다음 질문은 이것이다.

학습·검증·테스트 구간을 어떤 시간 순서로 나눠야 공정한 백테스트가 될까?

Sources

오류 제보·의견 보내기

글 제목과 주소가 포함된 메일 초안을 엽니다. 내용과 받는 사람을 확인한 뒤 보내주세요.

받는 사람: [email protected]

메일 앱에서 작성

메일 앱이 열리지 않으면 아래 내용을 복사해 평소 쓰는 이메일에 붙여 넣으세요.

문의 안내