퀀트 투자 입문 ② — 수익률 계산, 제대로 알고 시작하기
백테스트의 모든 결과물은 결국 수익률 계산 위에 서 있습니다. 그런데 수익률은 생각보다 헷갈리는 개념입니다. “10% 오르고 10% 떨어지면 본전”이라고 생각했다면 이번 글이 도움이 될 겁니다(정답: 1% 손실입니다). 이번 글에서는 단순 수익률, 로그 수익률, 누적 수익률, 연평균 수익률(CAGR)을 pandas 코드와 함께 정리합니다.
단순 수익률 (Simple Return)
가장 기본이 되는 수익률입니다. 어제 10,000원이던 주식이 오늘 10,500원이 되었다면:
수익률 = (10,500 - 10,000) / 10,000 = 0.05 = 5%
pandas에서는 pct_change() 한 줄입니다.
import FinanceDataReader as fdr
df = fdr.DataReader('005930', '2023-01-01') # 삼성전자
returns = df['Close'].pct_change().dropna()
print(returns.head())
첫 행은 비교할 전일 가격이 없어서 NaN이 되므로 dropna()로 제거합니다.
수익률은 더하면 안 된다
여기서 초보 시절 누구나 한 번은 빠지는 함정이 나옵니다. 단순 수익률은 기간에 걸쳐 더할 수 없습니다.
- 첫날 +10%, 둘째 날 -10%: 합산하면 0%로 보이지만
- 실제로는 1.10 × 0.90 = 0.99, 즉 -1%입니다.
수익률은 더하는 것이 아니라 (1 + 수익률)을 곱하는 것입니다. 이걸 복리(compounding)라고 부릅니다. 하락이 무서운 이유도 여기 있습니다. -50%가 나면 +50%로는 본전이 안 되고 +100%가 필요합니다.
누적 수익률 (Cumulative Return)
그래서 여러 기간의 성과는 곱셈으로 누적합니다.
cum_returns = (1 + returns).cumprod() - 1
print(f"기간 전체 수익률: {cum_returns.iloc[-1]:.2%}")
cum_returns.plot(figsize=(12, 5), title='Cumulative Return')
(1 + returns).cumprod()는 “1원을 투자했으면 지금 얼마가 됐나”를 매일 계산한 것이고, 거기서 1을 빼면 수익률이 됩니다. 백테스트 성과 그래프는 대부분 이 곡선입니다.
로그 수익률 (Log Return)
수익률을 곱셈이 아니라 덧셈으로 다루고 싶을 때 쓰는 것이 로그 수익률입니다.
import numpy as np
log_returns = np.log(df['Close'] / df['Close'].shift(1)).dropna()
# 로그 수익률은 그냥 더하면 누적이 된다
total_log = log_returns.sum()
print(f"단순 수익률로 환산: {np.exp(total_log) - 1:.2%}")
로그 수익률의 합에 exp를 씌우고 1을 빼면 누적 단순 수익률과 정확히 일치합니다. 통계 분석(평균, 분산, 정규성 검정)을 할 때는 로그 수익률이 다루기 편해서 학계 연구에서 로그 수익률을 많이 씁니다. 일상적인 백테스트에서는 단순 수익률의 cumprod 방식이면 충분하고, 둘을 섞어 쓰지만 않으면 됩니다.
연평균 수익률, CAGR
“3년 동안 40% 벌었다”는 말은 1년 기준으로 얼마인지 감이 안 옵니다. 그래서 기간이 다른 성과를 비교할 때는 CAGR(연평균 복리 수익률)로 환산합니다.
CAGR = (최종 자산 / 초기 자산)^(1/연수) - 1
n_years = (df.index[-1] - df.index[0]).days / 365.25
final_value = (1 + returns).prod()
cagr = final_value ** (1 / n_years) - 1
print(f"CAGR: {cagr:.2%}")
3년에 40%면 CAGR로 약 11.9%입니다. 어떤 전략의 성과든 CAGR로 환산해 보는 습관을 들이면 과장된 숫자에 속을 일이 줄어듭니다.
산술평균의 함정: 변동성 손실
“평균 수익률이 플러스면 돈을 번다”는 착각도 짚고 갑니다. 2년짜리 극단적인 예를 봅시다.
- 1년 차 +50%, 2년 차 -40%: 산술평균은 (50 - 40) / 2 = +5%
- 실제 계좌: 1.50 × 0.60 = 0.90, 즉 -10%
평균은 플러스인데 계좌는 마이너스입니다. 수익률이 출렁일수록 산술평균과 실제 복리 수익률의 격차가 커지는데, 이를 변동성 손실(volatility drag)이라고 부릅니다. 복리 수익률은 산술평균보다 대략 분산의 절반(σ²/2, σ는 변동성)만큼 낮습니다. 그래서 변동성이 커지면 격차는 변동성의 제곱에 비례해 빠르게 벌어집니다. 같은 평균 수익률이라면 덜 출렁이는 쪽의 실제 성과가 더 좋습니다. 저변동성 팩터나 자산 배분이 힘을 발휘하는 수학적 이유가 바로 이것입니다. 같은 변동성 손실이 매일 배수를 다시 맞추는 레버리지 ETF에서 얼마나 커지는지는 레버리지 ETF를 오래 들고 있으면 생기는 일에서 코스피 데이터로 확인했습니다.
# 산술평균 vs 실제 복리(기하평균) 비교
arith_mean = returns.mean() * 252 # 연환산 산술평균
geo_mean = (1 + returns).prod() ** (252 / len(returns)) - 1 # 연환산 복리
print(f"산술평균(연): {arith_mean:.2%} / 복리(연): {geo_mean:.2%}")
둘의 차이가 클수록 그 자산은 출렁임이 심하다는 뜻입니다.
기간 환산: 일별·월별·연간
수익률을 다른 기간 단위로 바꾸는 관례도 알아두면 편합니다. 1년은 보통 거래일 252일, 12개월로 놓고 계산합니다.
# 일별 수익률 → 월별 수익률 (그 달의 복리 누적)
monthly_returns = (1 + returns).resample('ME').prod() - 1
# 월별 수익률 → 연환산 (12개월 복리)
annualized = (1 + monthly_returns).prod() ** (12 / len(monthly_returns)) - 1
주의: returns.mean() * 252처럼 단순히 곱하는 연환산은 근사치일 뿐이고(위에서 본 변동성 손실이 무시됨), 정확한 값은 복리로 계산해야 합니다. 관행상 통계 지표(변동성, 샤프비율)에는 곱셈 연환산을 쓰지만, 성과 보고에는 복리 기준을 쓰는 것이 정직합니다.
자주 하는 실수 정리
- 수익률을 더해서 누적하기 — 반드시 (1+r)을 곱해야 합니다. 특히 손실 구간이 있으면 오차가 커집니다.
- 단순 수익률과 로그 수익률 섞어 쓰기 — 한 백테스트 안에서는 하나로 통일해야 합니다.
- 기간이 다른 수익률을 그대로 비교하기 — 6개월 수익률과 2년 수익률은 CAGR로 환산한 뒤 비교합니다.
- 평균 수익률로 성과를 말하기 — 일별 수익률의 산술평균이 양수여도 누적 수익률은 마이너스일 수 있습니다(변동성 손실). 성과는 누적 수익률과 CAGR로 말해야 합니다.
정리
- 수익률의 누적은 덧셈이 아니라 (1+r)의 곱셈이다.
- 통계 분석에는 로그 수익률, 성과 보고에는 단순 수익률 기반 누적 수익률과 CAGR을 쓴다.
- pandas에서는
pct_change(),cumprod(),prod()세 가지면 기본 계산이 끝난다.
다음 두 글은 코드를 잠시 내려놓고 개념을 정리합니다. 먼저 백테스트가 정확히 무엇인지, 그다음 모멘텀이 무엇인지를 다룬 뒤, 그 개념들을 실제 코드 30줄짜리 백테스트로 구현합니다. 여기서 계산한 누적 수익률 곡선이 전략 평가의 기준이 됩니다.
2026-09-28 수정: 월별 수익률을 연환산하는 예제 코드를 ‘(1 + 월평균 수익률)의 12제곱’에서 전체 기간 복리 기준으로 바로잡고, 로그 수익률의 합에 exp를 씌운 뒤 1을 빼야 누적 단순 수익률이 된다는 설명을 보탰습니다.
이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.