7편. 첫 머신러닝 모델: 로지스틱 회귀로 다음 리밸런싱 기간의 상승 확률을 검증하기
근거와 검증 범위 — 머신러닝 기준선 설계
특징·레이블·시간 분할·재학습·거래 규칙을 정의합니다. 확정된 ETF 데이터 스냅샷을 사용한 성과 보고서가 아니며, 실제 수익률이나 정확도 수치를 제시하지 않습니다.
정확도와 수익률은 다르다: 로지스틱 회귀로 설계하는 첫 ML 기준선
6편에서는 월말의 12개월 모멘텀 신호가 양수인 ETF만 보유하고, 그렇지 않으면 현금으로 대기하는 단순 기준선을 만들었다. 이번에는 비교 조건을 그대로 둔 채 신호 생성 방식만 바꾼다. 과거 수익률 규칙 대신 로지스틱 회귀가 추정한 다음 리밸런싱 기간의 상승 확률을 사용한다.
처음에는 머신러닝이 여러 특징을 함께 본다는 사실만으로 단순 모멘텀보다 나은 판단을 할 수 있을 것이라고 기대했다. 하지만 첫 모델을 구현하며 더 중요해진 질문은 따로 있었다.
모델의 정확도가 높은가가 아니라, 미래 정보를 쓰지 않고 같은 조건에서 기준선과 비교했는가?
이 글은 특정 ETF 데이터로 성과를 입증하는 결과 보고서가 아니다. 데이터 공급자와 가격 열, ETF 후보군, 관측 기간을 하나의 검증된 스냅샷으로 확정하지 않은 상태에서 수익률이나 정확도 숫자를 제시하면 재현할 수 없는 결과가 되기 때문이다. 대신 실제 실험에 필요한 특징, 레이블, 시간 분할, 재학습, 거래 규칙과 판정 기준을 하나의 실행 가능한 설계로 고정한다.
이 글의 코드와 설명은 교육·연구 목적이다. 특정 ETF나 종목의 매수·매도를 추천하지 않으며 수익이나 미래 성과를 보장하지 않는다. 백테스트는 고정된 데이터와 가정에서 계산한 가상 성과일 뿐이다. [S7]
이번 편에서 바꾸는 것과 바꾸지 않는 것
공정한 비교를 위해 6편에서 정한 조건은 유지한다.
| 항목 | 6편 단순 모멘텀 | 7편 로지스틱 회귀 |
|---|---|---|
| ETF 후보군과 가격 데이터 | 사전에 고정 | 동일한 스냅샷 사용 |
| 신호 시점 | 월말 | 동일 |
| 주문 반영 시점 | 다음 거래일부터 | 동일 |
| 리밸런싱 | 월 1회 | 동일 |
| 포지션 | 롱 또는 현금 | 동일 |
| 선택 자산 배분 | 동일가중 | 동일 |
| 비용 민감도 | 편도 0·5·10·20bp | 동일 |
| 신호 | 최근 12개월 수익률의 부호 | 상승 확률과 고정 임계값 |
바뀌는 것은 마지막 한 줄뿐이다. 모델이 더 복잡해졌다는 이유로 ETF 구성, 기간, 비용이나 거래 시점을 함께 바꾸면 성과 차이의 원인을 알 수 없다.
1. 예측 문제를 한 문장으로 고정한다
이번 모델이 답하려는 질문은 다음과 같다.
월말까지 관측할 수 있는 가격·변동성·거래량 특징으로 다음 월별 보유기간의 수익률 부호를 분류할 수 있는가?
양성 클래스는 다음 보유기간 수익률이 0보다 큰 경우다.
label_t = 1 if next_holding_return_t > 0
label_t = 0 otherwisescikit-learn의 LogisticRegression은 분류 모델이며, predict_proba로 클래스별 확률 추정치를 반환한다. [S1] 여기서 모델이 출력한 0.62는 현실에서 반드시 62% 확률로 상승한다는 뜻이 아니다. 학습 데이터와 모델 가정 아래에서 계산된 점수에 가깝다.
그래서 이 글에서는 다음 표현을 구분한다.
| 표현 | 의미 |
|---|---|
| 상승 확률 | 모델이 출력한 양성 클래스의 추정 확률 |
| 분류 정확도 | 예측한 클래스와 실제 레이블이 일치한 비율 |
| 전략 수익률 | 그 예측을 거래 규칙으로 바꿨을 때의 가상 성과 |
| 투자 가능성 | 비용, 유동성, 체결과 위험까지 고려한 별도의 판단 |
분류가 조금 더 정확하더라도 틀린 거래의 손실이 크거나 매매가 잦으면 비용 후 성과는 나빠질 수 있다. 따라서 정확도와 투자 성과는 같은 표에 섞지 않고 별도로 본다.
2. 특징과 레이블의 시간을 분리한다
이번 모델의 특징 후보는 모두 월말 시점에 이미 관측할 수 있는 값으로 제한한다.
| 특징 | 계산 범위 | 월말에 관측 가능한가 |
|---|---|---|
| 최근 1일 수익률 | 월말까지 1거래일 | 예 |
| 최근 5일 수익률 | 월말까지 5거래일 | 예 |
| 최근 20일 수익률 | 월말까지 20거래일 | 예 |
| 이동평균 차이 | 20일·120일 이동평균 | 예 |
| 최근 변동성 | 최근 20일 일별 수익률 표준편차 | 예 |
| 거래량 변화 | 최근 거래량과 이전 평균의 차이 | 예 |
| 시장 수익률 | 시장 프록시의 최근 20일 수익률 | 예 |
| 다음 보유기간 수익률 | 다음 리밸런싱까지 | 아니오, 레이블에만 사용 |
같은 데이터 행에 들어 있다는 사실은 같은 시점에 알 수 있었다는 뜻이 아니다. 다음 달 수익률을 현재 행에 붙이는 것은 학습용 레이블을 만들기 위한 정렬일 뿐, 예측 시점의 입력으로 사용해도 된다는 뜻이 아니다.
monthly["forward_return"] = (
monthly.groupby("symbol")["execution_close"]
.transform(lambda s: s.shift(-1).div(s).sub(1.0))
)
monthly["label"] = monthly["forward_return"].gt(0).astype("int8")shift(-1)은 다음 행의 결과를 현재 신호 행에 맞춘다. [S4] 마지막 행처럼 다음 보유기간이 끝나지 않은 관측치는 레이블이 없으므로 학습과 평가에서 제외한다.
여기서 한 단계 더 확인해야 한다. 월말 t에 모델을 다시 학습할 때는 과거 신호 행이라도 보유기간이 아직 끝나지 않아 레이블이 확정되지 않았다면 학습에 넣으면 안 된다.
eligible_train = samples[
(samples["signal_date"] < current_signal_date)
& (samples["label_end_date"] < current_signal_date)
]이 조건이 빠지면 코드상으로는 과거 행만 골랐더라도 아직 알 수 없던 결과가 학습에 들어갈 수 있다.
3. 무작위 분할 대신 달력 순서를 지킨다
일반적인 분류 예제처럼 데이터를 무작위로 섞어 학습·테스트로 나누면 금융 시계열에서는 미래 정보가 과거 평가에 섞일 수 있다. TimeSeriesSplit은 시간 순서에 따른 분할과 학습·검증 사이의 gap을 지원한다. [S2]
이번 실험은 다음 순서를 사용한다.
과거 미래
|──────── 학습 ────────|──── 검증 ────|──── 테스트 ────|
모델 적합 선택 확정 최종 평가- 학습 구간에서는 모델과 전처리 규칙을 적합한다.
- 검증 구간에서는 사전에 선언한 소수의 임계값과 모델 설정만 비교한다.
- 테스트 구간은 선택을 모두 끝낸 뒤 한 번 평가한다.
- 테스트 성과를 본 뒤 특징이나 임계값을 바꾸면 그 구간은 더 이상 테스트가 아니다.
기간 경계는 데이터의 실제 시작일과 시장 국면을 확인한 뒤 실험 기록에 함께 남긴다. 중요한 것은 특정 연도가 아니라, 결과를 보기 전에 경계를 고정하고 시간 순서를 되돌리지 않는 것이다.
4. 전처리도 학습 구간에서만 배운다
표준화는 미래를 보지 않는 것처럼 보이지만, 전체 데이터의 평균과 표준편차로 fit하면 테스트 구간의 분포가 학습 과정에 들어간다. scikit-learn은 이러한 전처리 누수를 피하기 위해 학습 데이터에만 fit하고 다른 구간에는 transform을 적용할 것을 권장한다. [S3]
가장 간단한 방법은 전처리와 모델을 하나의 Pipeline으로 묶는 것이다.
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
model = Pipeline(
steps=[
("scale", StandardScaler()),
(
"classifier",
LogisticRegression(
C=1.0,
max_iter=2_000,
random_state=7,
),
),
]
)
model.fit(train[feature_columns], train["label"])
probability = model.predict_proba(current[feature_columns])
classes = model.named_steps["classifier"].classes_
positive_class_index = list(classes).index(1)
current["up_probability"] = probability[:, positive_class_index]양성 확률이 항상 배열의 두 번째 열일 것이라고 가정하지 않고 classes_에서 클래스 1의 위치를 확인한다. 모델에 들어가기 전에는 결측값뿐 아니라 inf와 -inf도 검사한다. 특히 이전 평균 거래량이 0인 상태에서 변화율을 계산하면 무한대가 생길 수 있으므로 조용히 대체하기보다 데이터 오류로 중단하는 편이 안전하다.
5. 한 번 학습하고 끝내지 않고 월별로 다시 걷는다
실제 월말에 이 전략을 사용한다고 가정하면 그 시점까지 확정된 자료만 사용할 수 있다. 따라서 테스트 전체를 한꺼번에 예측하기보다 월별 walk-forward 방식으로 모델을 다시 적합한다.
predictions = []
for signal_date in test_signal_dates:
train = samples[
(samples["signal_date"] < signal_date)
& (samples["label_end_date"] < signal_date)
].copy()
current = samples[samples["signal_date"] == signal_date].copy()
model.fit(train[feature_columns], train["label"])
probability = model.predict_proba(current[feature_columns])
classes = model.named_steps["classifier"].classes_
positive_index = list(classes).index(1)
current["up_probability"] = probability[:, positive_index]
predictions.append(current)학습창은 과거 자료가 계속 쌓이는 확장창을 기본으로 삼는다. 고정 길이 창을 쓰고 싶다면 길이를 결과를 보기 전에 정해야 한다. 월별 재학습을 쓰는 이유도 성과를 높이기 위해서가 아니라, 매월 실제로 이용 가능했던 정보 집합을 재현하기 위해서다.
6. 확률을 거래 규칙으로 바꾼다
모델 점수만으로는 백테스트가 되지 않는다. 어떤 확률부터 보유할지, 여러 ETF가 선택되면 어떻게 배분할지, 아무것도 선택되지 않으면 무엇을 할지 정해야 한다.
이번 편의 기본 규칙은 단순하게 고정한다.
월말에 ETF별 상승 확률을 계산한다.
상승 확률이 0.50 이상인 ETF가 있으면
해당 ETF들을 동일가중으로 보유한다.
조건을 만족한 ETF가 없으면
현금 비중을 100%로 둔다.
새 비중은 다음 거래일부터 반영한다.0.50은 금융시장에 최적인 값이라는 뜻이 아니다. 결과를 보기 전에 정한 첫 기준값이다. 여러 임계값을 계속 바꾸며 가장 높은 성과만 고르면 과거 데이터에 맞춘 선택일 가능성이 커진다. 백테스트를 많이 반복할수록 표본 외 성과가 재현되지 않을 위험이 커진다는 점을 함께 기록해야 한다. [S6]
추가 탐색이 필요하다면 검증 구간에서만 0.45, 0.50, 0.55처럼 사전에 선언한 제한된 후보를 비교한다. 테스트 구간에 들어간 뒤에는 선택된 임계값을 바꾸지 않는다.
7. 정확도와 투자 성과를 분리해서 본다
모델 평가는 두 층으로 나눈다.
첫 번째는 분류 자체에 대한 평가다.
- 테스트 관측치 수
- 정확도
- 양성 예측 비율
- 실제 양성 비율
- 확률 분포
두 번째는 신호를 동일한 거래 규칙에 넣었을 때의 경제적 평가다.
- 비용 전·후 누적수익률
- 연환산 수익률
- 변동성
- 최대낙폭
- Sharpe ratio와 그 계산 가정
- 총 turnover
- 현금 보유 비율
금융 분류 연구에서도 분류 정확도와 누적수익률은 별도의 결과로 비교된다. 다만 특정 시장과 주기로 얻은 연구 결과를 월별 ETF 전략에 그대로 일반화할 수는 없다. [S5]
판정은 하나의 숫자로 하지 않는다.
| 관찰 | 해석할 질문 |
|---|---|
| 정확도는 높고 비용 후 수익률은 낮다 | 틀린 거래의 손실 크기나 turnover가 증가했는가 |
| 수익률은 높고 정확도는 비슷하다 | 일부 큰 상승 구간의 영향이 과도하지 않은가 |
| 최대낙폭은 낮고 현금 비중은 높다 | 위험 감소가 단순히 시장 노출 감소에서 왔는가 |
| 0bp에서는 우세하고 10bp부터 역전된다 | 신호의 경제적 이점이 거래비용보다 작은가 |
| 특정 기간에만 우세하다 | 한 시장 국면에 과도하게 의존하는가 |
이 표의 목적은 모델이 이겼다는 이야기를 만들기보다, 결과가 왜 달라졌는지를 추적하는 데 있다.
8. 결과 숫자를 싣지 않은 이유
이번 글에는 특정 ETF 후보군, 데이터 공급자, 조정가격 산식, 원본 파일 해시와 관측 기간을 확정한 데이터 스냅샷이 포함되어 있지 않다. 이 상태에서 임의의 숫자를 채우는 것은 독자가 다시 확인할 수 없는 성과를 만드는 일이다.
따라서 이번 편에서 확정한 산출물은 수익률 숫자가 아니라 다음의 실험 계약이다.
- 특징은 월말까지 관측 가능한 값만 사용한다.
- 레이블은 다음 보유기간 수익률의 부호로 정의한다.
- 학습에는 현재 신호일 전에 종료된 레이블만 사용한다.
- 전처리는 매 학습창 안에서만 적합한다.
- 임계값은 검증 구간에서 확정하고 테스트에서 바꾸지 않는다.
- 6편과 같은 자산, 기간, 실행 시점과 비용을 사용한다.
- 정확도와 경제적 성과를 별도로 평가한다.
실제 수치를 공개할 때는 최소한 ETF 목록, 데이터 공급자, 가격 열 정의, 다운로드 시각, SHA-256 해시, 기간 경계, 코드 버전과 비용 가정을 함께 제공해야 한다. 비용은 bid-ask spread뿐 아니라 주문 규모, 시장 상황과 체결 방식에 따라 달라질 수 있으므로 고정 bp 값은 어디까지나 민감도 가정이다. [S8]
숫자가 없다는 사실은 실험의 성공을 뜻하지도 실패를 뜻하지도 않는다. 현재 단계에서 할 수 있는 정직한 결론은 “비교 가능한 실험 규칙을 고정했다”는 데까지다.
9. 이 모델이 아직 답하지 못하는 것
이 설계가 데이터 누수 가능성을 줄여 주더라도 다음 문제까지 해결해 주지는 않는다.
- 사용한 가격 데이터가 분배금과 분할을 올바르게 반영했는가
- ETF 후보군에 생존자 편향이 없는가
- 모델의 확률이 실제 빈도와 잘 맞도록 보정됐는가
- 종가 기반 가정이 실제 체결가격과 얼마나 다른가
- 고정 비용 시나리오가 개인의 수수료와 세금을 반영하는가
- 다른 시장 국면에서도 같은 관계가 유지되는가
- 여러 실험 중 좋은 설정만 골라낸 것은 아닌가
과거 백테스트는 가상의 성과이며 미래 수익을 예측하거나 보장하지 않는다. [S7] 이 한계를 결과와 함께 공개하지 않으면 모델의 정교함이 오히려 잘못된 확신을 만들 수 있다.
10. 첫 ML 모델을 만들고 바뀐 판단
로지스틱 회귀를 선택한 이유는 최신이거나 강력해서가 아니다. 입력 특징과 확률 출력의 관계를 비교적 단순하게 추적할 수 있고, 이후 더 복잡한 모델을 평가할 기준점으로 쓰기 좋기 때문이다.
이번 작업 전에는 여러 특징을 학습하는 모델이라면 단순 모멘텀보다 더 많은 정보를 활용할 것이라고 생각했다. 지금은 생각의 순서가 달라졌다.
더 많은 정보를 넣는 것보다, 그 정보가 당시에도 관측 가능했는지를 먼저 확인해야 한다.
또한 높은 정확도는 전략의 우월성을 의미하지 않는다. 모델이 기준선을 이겼다고 말하려면 같은 데이터와 실행 규칙에서 비용 후 수익률, 최대낙폭과 turnover까지 함께 좋아졌는지 확인해야 한다. 그 결과가 한 번의 테스트 구간에만 나타났다면 결론은 더 제한적으로 써야 한다.
이번 편에서 얻은 것은 수익을 내는 공식이 아니라, 다음 모델을 의심할 기준이다.
다음 편: 복잡한 모델은 정말 더 나을까
8편에서는 같은 특징과 시간 분할, 같은 포트폴리오 규칙 아래에서 의사결정나무와 랜덤 포레스트 또는 gradient boosting 계열 모델 하나를 로지스틱 회귀와 비교한다.
질문은 “트리 모델이 더 높은 수익률을 냈는가”에서 끝나지 않는다.
- 비선형 관계를 학습한 이점이 테스트 구간에도 남았는가
- 하이퍼파라미터 탐색 횟수가 얼마나 늘었는가
- 성과 차이가 비용 후에도 유지됐는가
- 특정 기간이나 일부 ETF가 결과를 지배했는가
- 복잡성이 늘어난 만큼 설명 가능성과 재현성이 낮아지지 않았는가
복잡한 모델의 가치는 이름이 아니라 단순한 기준선을 같은 조건에서 안정적으로 넘어설 때 생긴다. 그리고 넘어섰다는 판단은 정확도가 아니라 전체 실험 기록으로 내려야 한다.
Sources
- [S1] LogisticRegression — scikit-learn documentation | scikit-learn developers | https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html ↩
- [S2] TimeSeriesSplit — scikit-learn documentation | scikit-learn developers | https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html ↩
- [S3] Common pitfalls and recommended practices — scikit-learn documentation | scikit-learn developers | https://scikit-learn.org/stable/common_pitfalls.html ↩
- [S4] pandas.DataFrame.shift — pandas documentation | pandas development team | https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.shift.html ↩
- [S5] Evaluating machine learning classification for financial trading: an empirical approach | Eduardo A. Gerlein, Martin McGinnity, Ammar Belatreche, Sarah Coleman | 2016 | https://irep.ntu.ac.uk/id/eprint/28062/ ↩
- [S6] Pseudo-Mathematics and Financial Charlatanism: The Effects of Backtest Overfitting on Out-of-Sample Performance | David H. Bailey, Jonathan M. Borwein, Marcos López de Prado, Qiji Jim Zhu | 2014 | https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2308659 ↩
- [S7] Investor Bulletin: Performance Claims | U.S. Securities and Exchange Commission, Office of Investor Education and Advocacy | 2022 | https://www.investor.gov/introduction-investing/general-resources/news-alerts/alerts-bulletins/investor-bulletins-47 ↩
- [S8] Exchange-Traded Funds | U.S. Securities and Exchange Commission | 2019 | https://www.sec.gov/files/rules/final/2019/33-10695.pdf ↩
오류 제보·의견 보내기
글 제목과 주소가 포함된 메일 초안을 엽니다. 내용과 받는 사람을 확인한 뒤 보내주세요.
받는 사람: [email protected]
메일 앱에서 작성메일 앱이 열리지 않으면 아래 내용을 복사해 평소 쓰는 이메일에 붙여 넣으세요.
관련 글
퀀트·데이터 연구 로지스틱 회귀는 기준선을 넘을까? 공정한 모델 비교의 조건
로지스틱 회귀와 기준 전략을 공정하게 비교하기 위한 시간축, 데이터 분리, 재학습과 비용 계산 규칙을 정리합니다. 실제 성과 검증에 앞서 고정할 실험 설계입니다.
퀀트·데이터 연구 shift(1)만으로는 부족하다: 신호·체결·레이블의 시간 계약
신호 생성, 주문 체결, 수익 귀속과 레이블 확정 시점을 구분합니다. 합성 단위테스트로 미래정보 누수와 전처리의 시간 경계를 점검합니다.