06. 시차가 다른 시장의 종가를 피처로 쓰기

2026. 8. 31. 19:53카테고리 없음

화면 뒤의 데이터 판단 · 방향 신호

시차가 다른 시장의 종가를 피처로 쓰기

아시아·유럽 종가는 미국 개장 전에 확정된다

미국 지수의 5일 뒤 방향을 맞히는 모델에서, 가격·추세·변동성 피처만으로는 홀드아웃 AUC 가 0.5088 이었습니다. 사실상 동전던지기입니다.

여기서 피처를 더 정교하게 만드는 대신 정보의 출처를 바꿨습니다. 미국 장이 열리기 전에 이미 확정된 값이 있습니다.

마감 시각은 정보 확정 시각이다

같은 날짜 행에 나란히 놓인 값이라도 확정 시각은 다릅니다. 도쿄·홍콩·상하이·서울이 먼저 닫고, 그다음 프랑크푸르트·런던이 닫고, 마지막이 뉴욕입니다.

그래서 t일 미국 종가를 예측하는 시점에 t일 아시아·유럽 종가는 이미 알려져 있습니다. 날짜만 보면 "같은 날" 이라 미래처럼 보이지만, 시계로 보면 과거입니다.

아시아 마감
도쿄·홍콩·상하이·서울 — t일 값 확정
유럽 마감
프랑크푸르트·런던 — t일 값 확정
미국 개장·마감
우리가 예측하는 대상 — 아직 모름

초록 = 예측 시점에 이미 알려진 값 · 빨강 = 예측 대상. 같은 t일이지만 확정 순서가 다르다.

반대 방향은 성립하지 않습니다. t일 미국 종가를 t일 아시아 모델에 넣으면 그건 진짜 누수입니다. 이 정보에는 방향이 있습니다.

두 블록으로 나눴다

① 지역 수익률

아시아 4개(^N225·^HSI·000001.SS·^KS11), 유럽 3개(^GDAXI·^FTSE·^STOXX50E)의 평균 수익률을 1일·5일로 뽑았습니다.

개별 지수를 그대로 넣지 않고 지역 평균을 쓴 이유는, 개별 시장의 국내 이슈를 지우고 "지역 전체가 어느 쪽으로 움직였나" 만 남기기 위해서입니다.

아시아 4
^N225 · ^HSI · 000001.SS · ^KS11
지역 평균
개별 시장의 국내 이슈를 지운다
파생 2개
지역 격차 · 부호 일치
유럽 3
^GDAXI · ^FTSE · ^STOXX50E
지역 평균
1일·5일 두 지평으로 뽑는다
합계 6개
국제 선행 블록

개별 지수를 그대로 넣지 않는다 — 지역 단위로 묶어야 "어느 쪽으로 움직였나"만 남는다.

여기에 두 개를 더 붙였습니다.

  • ld_asia_minus_euro1 — 아시아가 유럽보다 먼저다. 둘의 격차는 하루 안에서 흐름이 이어졌는지 꺾였는지를 담는다
  • ld_regions_agree — 두 지역의 5일 수익률 부호가 같은지. 전역 충격과 지역 충격을 가른다
ASIA = ['^N225', '^HSI', '000001.SS', '^KS11']
EURO = ['^GDAXI', '^FTSE', '^STOXX50E']

a1, e1 = region_ret(ASIA, 1), region_ret(EURO, 1)
a5, e5 = region_ret(ASIA, 5), region_ret(EURO, 5)
# 아시아가 유럽보다 먼저다. 격차 = 하루 안에서 흐름이 이어졌나
out['ld_asia_minus_euro1'] = a1 - e1
# 두 지역이 같은 방향인가 — 전역 충격과 지역 충격을 가른다
out['ld_regions_agree'] = np.sign(a5.fillna(0)) * np.sign(e5.fillna(0))

② 야간/장중 분해

같은 발상을 미국 지수 자체에도 적용했습니다. 하루 수익률을 통째로 쓰지 않고 둘로 쪼갭니다.

야간 (on)
전일 종가 → 당일 시가
해외 시간대에 형성된다
장중 (id)
시가 → 종가
미국 참여자가 만든다

둘을 합친 하루 수익률만 쓰면 "해외발이었나 국내발이었나"가 지워진다. 격차와 부호 일치를 따로 준다.

# 야간 = 전일 종가 → 당일 시가. 해외 시간대에 형성된다
on = op / cl.shift(1) - 1
# 장중 = 시가 → 종가. 미국 참여자가 만든다
idr = cl / op - 1
# 격차 — 해외발 압력과 국내발 압력 중 어느 쪽이 셌나
out['ld_on_minus_id5'] = out['ld_on5'] - out['ld_id5']
# 부호 일치 — 밤과 낮이 같은 방향이면 추세, 반대면 되돌림
out['ld_on_id_agree'] = np.sign(on.fillna(0)) * np.sign(idr.fillna(0))

야간 수익률은 해외 시간대에 만들어지고, 장중은 미국 참여자가 만듭니다. 둘을 합쳐 버리면 "해외발 압력이 셌나, 국내발이 셌나" 가 지워집니다.

한 칸 미는 비용을 받아들였다

이 블록의 마지막 줄은 return out.shift(1) 입니다.

원리대로면 밀 필요가 없습니다. 아시아 종가는 미국 종가보다 먼저 확정되니 같은 날 값을 그대로 써도 됩니다.

문제는 휴장입니다. 나라마다 공휴일이 다르고, 시계열을 미국 거래일 인덱스에 맞춰 ffill 로 정렬하는 과정에서 어느 날짜의 값이 어느 행에 실렸는지가 흐려집니다. 한 군데라도 어긋나면 미래가 섞입니다.

반나절치 정보 이득을 포기했다 — 확실히 과거인 값만 쓰도록 한 칸 뒤로 밀었습니다. 성능을 조금 잃는 쪽이, 나중에 누수로 결과 전체를 철회하는 것보다 쌉니다.

이 피처가 지나는 경로

피처 하나가 화면 숫자가 되기까지 두 개의 레인을 지납니다. 위는 오프라인 연구, 아래는 매일 도는 운영입니다.

연구 — 한 번, 오프라인
수집
yfinance 로 해외 지수 7개 + 대상 ETF 일봉
피처 조립
지역 평균·격차·부호일치 + 야간/장중 12개
워크포워드
창을 옮기며 학습·검증, 홀드아웃은 봉인
산출물
pkl + _meta.json
운영 — 매일, 배치
모델 로드
us_ts_signal 이 pkl 을 캐시에 올림
피처 재계산
같은 build() 를 불러 마지막 행만
결합·적재
크기 × 방향 부호, 하루 1회 DB 저장
화면
AI차트 경로·밴드 / AI 종합판단

파란 칸 두 개는 같은 코드다 — 연구의 피처 조립 함수를 운영이 그대로 import 한다.

같은 흐름을 단계별로 풀면 이렇습니다.

[수집]  yfinance — 해외 지수 7개 + 대상 ETF 의 일봉
   │      심볼별로 받아 프로세스 안에 캐시한다(같은 심볼 재조회 금지)
   ▼
[피처]  wf_lead.l_block() — 지역 평균·격차·부호일치 + 야간/장중 12개
   │      미국 거래일 인덱스에 맞춰 정렬하고 마지막에 한 칸 민다
   ▼
[학습]  워크포워드 — 창을 옮겨 가며 학습·검증, 홀드아웃은 손대지 않는다
   │      개발 구간(~2024-12-31)에서만 구성을 고른다
   ▼
[산출]  models/방향신호_미국_5_2.pkl + _meta.json
   │      pkl 옆에 평가방법·학습데이터·성능·문턱을 같이 적는다
   ▼
[추론]  processor/us_ts_signal.py — 같은 build() 를 불러 마지막 행만 예측
   │      학습·운영이 다른 코드를 쓰면 그 차이가 곧 오차가 된다
   ▼
[결합]  drift_path — 크기모델 × 방향모델 부호
   │      pr[h] = mag_hat[h] × sign(p_dir - 0.5), 학습 파라미터 없음
   ▼
[배치]  run_chart_predict_batch — 하루 1회 계산해 DB 에 적재
   │      요청이 올 때 계산하지 않는다(응답은 읽기만)
   ▼
[화면]  AI차트 30일 경로·밴드 · AI 종합판단의 방향 요인
          화면은 저장된 값을 읽어 그리기만 한다
운영이 연구 코드를 그대로 부른다 — 추론 시점에 wf_inter.build() 를 import 해서 씁니다. 피처를 운영용으로 다시 구현하면 그 미세한 차이가 학습·서빙 불일치가 되어, 검증에서 본 성능이 화면에서 재현되지 않습니다. 코드를 한 벌만 두는 쪽을 택했습니다.

모델 문턱이 0.5 가 아니라는 점도 이 경로에서 흡수합니다. h=5 의 문턱은 0.6322 인데, 추론 함수가 pkl 에 담긴 문턱을 읽어 0.5 중심으로 옮긴 확률을 돌려줍니다. 호출부는 0.5 기준으로만 판단하면 됩니다.

결과

기준선에서 한 단계에 한 블록씩만 바꿔 검증했습니다.

구성 더한 것 홀드아웃 AUC
v42 가격 피처(분수차분 포함) 0.5088
v57 국제 선행 · 야간/장중 12개 0.5489
v59 선물·통화·원자재 10개 0.5516
v62 국면 교차항 10개 0.5634
v42 가격        ██████████░░░░░░░░░░░░░░  0.5088
v57 국제 선행   ████████████████░░░░░░░░  0.5489
v59 선물·통화   █████████████████░░░░░░░  0.5516
v62 국면 교차   ███████████████████░░░░░  0.5634
                ·········┊··············  ← ┊ = 0.5(무작위)

막대는 AUC 0.44~0.60 구간을 채운 비율. 두 번째 줄이 이 글의 단계이며, 네 단계 중 가장 큰 폭이 여기서 나왔다.

가장 큰 폭(+0.0401)이 이 단계에서 나왔습니다.

뒤이은 두 단계도 같은 축입니다. 선물·엔·달러는 장 밖 정보가 지수보다 먼저 찍히는 자리고, 국면 교차항은 그 정보를 어느 변동성 국면에서 얼마나 믿을지를 모델이 배우게 한 것입니다.

그래도 채택하지 않았다

홀드아웃(2025-01 ~ 2026-06)에서는 SPY·QQQ·DIA·IWM 4개 전부 엣지가 양수였습니다. 그런데 사전등록해 둔 1차 판정은 통과하지 못했습니다.

과거 깨끗 구간을 통합해 재면 n=1,843 에 AUC 0.5076, 엣지 −0.16 / −0.20 입니다. 창별로는 2019-20 이 0.5886 인데 2010-11 은 0.4514 입니다.

2019-20         ██████████████████████░░  0.5886
2021-22         ███████████████░░░░░░░░░  0.5399
2014-15         ████████████░░░░░░░░░░░░  0.5192
2010-11         ██░░░░░░░░░░░░░░░░░░░░░░  0.4514
                ·········┊··············  ← ┊ = 0.5(무작위)

같은 눈금(0.44~0.60). 2010-11 만 0.5 표시 왼쪽에 머문다 — 강한 구간은 변동성이 컸던 시기와 겹친다.

 이 신호는 항상 있는 게 아니라 변동성이 컸던 구간에만 있습니다. 조용한 장에서는 해외 종가가 미국의 다음 5일에 대해 할 말이 없습니다.

그래서 운영 배선은 바꾸지 않았습니다. 홀드아웃 4/4 는 채택 근거지만, 판정 창 미통과는 보류 근거입니다. 둘 다 기록에 남깁니다.

정리

  • 날짜가 같아도 확정 시각은 다르다 — 마감이 먼저인 시장의 종가는 미래가 아니라 과거다
  • 개별 지수 대신 지역 평균 — 국내 이슈가 지워지고 방향만 남는다
  • 하루 수익률을 야간/장중으로 쪼갠다 — 해외발 압력과 국내발 압력이 분리된다
  • 정렬이 불확실하면 한 칸 민다 — 잃는 성능보다 누수 비용이 크다
  • 피처를 정교하게 만들기 전에, 아직 안 쓴 정보가 있는지 먼저 본다
  • 성능이 올라도 구간별로 갈리면 채택은 별개 판단이다

모델 구조가 아니라 정보가 확정되는 순서를 본 것이 가장 큰 한 걸음이었습니다. 그리고 그 한 걸음으로도 운영에 넣기에는 모자랐다는 것을, 같은 무게로 적어 둡니다.

화면 뒤의 데이터 판단 · 수집 → 피처 — 방향 신호