한 종목을 맞히는 게임에서 벗어나기
지금까지 우리는 이동평균선, 모멘텀, 가치 지표처럼 하나의 아이디어를 전략으로 바꾸는 방법을 배웠다. 하지만 실제 투자는 한 종목의 매수·매도 신호만으로 끝나지 않는다. 더 중요한 질문이 남아 있다.
어떤 종목을 얼마나 살 것인가?
같은 전략이라도 한 종목에 전부 투자하면 결과가 크게 흔들린다. 운 좋게 고른 종목이 오르면 높은 수익을 얻지만, 반대로 악재가 생기면 계좌 전체가 무너질 수 있다. 개별 기업에는 예측하기 어려운 사건이 많다. 실적 쇼크, 규제, 소송, 경영진 문제, 산업 변화 같은 일은 데이터만으로 완전히 피하기 어렵다.
포트폴리오 백테스트의 목적은 '가장 좋은 종목 하나'를 찾는 것이 아니다. 여러 종목을 함께 보유했을 때 전략 전체가 얼마나 안정적인지 확인하는 것이다. 좋은 퀀트 전략은 종목 선택 규칙뿐 아니라 비중 배분, 리밸런싱, 거래비용, 벤치마크 비교까지 함께 설계되어야 한다.
AI 생성 이미지
포트폴리오 수익률의 기본 구조
포트폴리오 수익률은 어렵지 않다. 각 종목의 수익률에 투자 비중을 곱한 뒤 모두 더하면 된다.
예를 들어 A, B, C 세 종목을 각각 40%, 30%, 30% 보유한다고 하자. 어느 날 수익률이 A 1%, B -2%, C 0.5%라면 포트폴리오 수익률은 다음과 같다.
0.4 * 0.01 + 0.3 * (-0.02) + 0.3 * 0.005결과는 -0.0005, 즉 -0.05%다. 한 종목은 올랐고 한 종목은 내렸지만, 전체 계좌는 비중에 따라 움직인다.
pandas로는 더 간단하게 계산할 수 있다. prices가 여러 종목의 수정종가 데이터프레임이라고 하자.
import pandas as pd
import numpy as np
returns = prices.pct_change().dropna()
weights = pd.Series({
'AAPL': 0.4,
'MSFT': 0.3,
'NVDA': 0.3
})
portfolio_return = (returns * weights).sum(axis=1)
wealth = (1 + portfolio_return).cumprod()여기서 핵심은 weights다. 포트폴리오 백테스트는 결국 비중을 어떻게 정하고, 언제 바꾸는지의 문제다.
한 종목 집중과 분산투자 비교하기
분산투자의 효과는 말로 설명하는 것보다 수익률 곡선으로 보는 편이 빠르다. 한 종목에만 투자한 경우와 여러 종목을 동일한 비중으로 보유한 경우를 비교해보자.
single_return = returns['AAPL']
equal_return = returns.mean(axis=1)
result = pd.DataFrame({
'single_stock': (1 + single_return).cumprod(),
'equal_weight': (1 + equal_return).cumprod()
})
result.plot(figsize=(12, 5))그래프를 보면 단일 종목이 더 높은 수익률을 보일 때도 있다. 분산투자는 항상 수익률을 높여주는 마법이 아니다. 대신 특정 종목에 생기는 충격을 줄여준다. 한 종목이 크게 하락해도 다른 종목이 버텨주면 전체 계좌의 낙폭은 줄어들 수 있다.
투자에서 중요한 것은 최고 수익률만이 아니다. 오래 살아남는 것도 중요하다. 수익률이 좋아 보여도 중간에 -60%, -70%를 견뎌야 한다면 대부분의 투자자는 전략을 계속 유지하지 못한다. 포트폴리오는 수익률을 조금 포기하더라도 전략을 지속할 수 있는 구조를 만드는 도구다.
동일비중 포트폴리오
가장 단순한 방식은 동일비중이다. 10개 종목을 보유한다면 각 종목을 10%씩 산다. 복잡한 예측이 필요 없고, 특정 종목에 대한 과도한 확신도 줄일 수 있다.
def equal_weight(past_prices):
n = len(past_prices.columns)
return pd.Series(1 / n, index=past_prices.columns)동일비중의 장점은 명확하다. 이해하기 쉽고 구현하기 쉽다. 특정 대형주에만 비중이 쏠리지 않는다. 소형주나 중형주가 포함된 전략에서는 동일비중이 시가총액 비중보다 더 공격적인 성격을 가질 수 있다.
하지만 단점도 있다. 규모가 작은 종목과 큰 종목을 같은 비중으로 사기 때문에 유동성 문제가 생길 수 있다. 거래대금이 적은 종목을 많이 사야 하는 전략이라면 실제 매매에서 가격 충격이 커질 수 있다. 백테스트에서는 잘 보이지 않지만 실전에서는 중요한 문제다.
시가총액 비중 포트폴리오
시가총액 비중은 기업의 시장 규모에 따라 비중을 정하는 방식이다. 시가총액이 큰 기업은 더 많이, 작은 기업은 더 적게 담는다. 많은 시장지수가 이 방식에 가깝다.
def market_cap_weight(market_caps, date):
caps = market_caps.loc[date].dropna()
return caps / caps.sum()시가총액 비중의 장점은 시장 전체의 구조를 자연스럽게 반영한다는 점이다. 대형주는 일반적으로 거래가 활발하고 매매 비용이 낮다. 그래서 실제 운용 가능성이 높다.
반면, 이미 많이 오른 대형주에 비중이 커지는 특징이 있다. 시장이 특정 업종이나 몇몇 대형주에 쏠릴 때 포트폴리오도 함께 쏠린다. 분산투자처럼 보이지만 실제로는 상위 몇 개 종목이 대부분의 움직임을 결정할 수 있다.
초보자는 먼저 동일비중으로 전략의 기본 성격을 확인하고, 이후 시가총액 데이터를 구할 수 있을 때 시가총액 비중과 비교해보는 순서가 좋다.
변동성 기반 비중 조절
세 번째 방식은 변동성이 큰 종목의 비중을 줄이고, 변동성이 작은 종목의 비중을 늘리는 것이다. 여기서 변동성은 수익률이 얼마나 크게 흔들리는지를 뜻한다.
간단한 방법은 최근 60일 변동성의 역수를 비중으로 사용하는 것이다. 변동성이 높을수록 역수는 작아지고, 변동성이 낮을수록 역수는 커진다.
def inverse_vol_weight(past_prices, lookback=60):
recent_returns = past_prices.pct_change().dropna().tail(lookback)
vol = recent_returns.std()
inv_vol = 1 / vol.replace(0, np.nan)
weight = inv_vol / inv_vol.sum()
return weight.fillna(0)이 방식은 포트폴리오의 흔들림을 줄이는 데 도움이 될 수 있다. 특히 변동성이 매우 큰 종목이 포트폴리오 전체를 지배하는 상황을 막아준다.
하지만 조심할 점이 있다. 변동성이 낮다고 해서 안전한 것은 아니다. 조용히 움직이던 종목도 어느 날 갑자기 크게 하락할 수 있다. 또 변동성 기반 비중은 과거 데이터를 기준으로 계산한다. 미래의 위험을 완벽히 알려주지는 않는다.
리밸런싱: 비중을 다시 맞추는 일
시간이 지나면 처음 정한 비중은 바뀐다. 어떤 종목은 오르고, 어떤 종목은 내리기 때문이다. 처음에 A와 B를 50%씩 샀더라도 A가 크게 오르면 A의 비중은 60%, 70%로 커질 수 있다.
리밸런싱은 포트폴리오 비중을 다시 목표 비중으로 맞추는 작업이다. 매월 할 수도 있고, 분기마다 할 수도 있고, 반기마다 할 수도 있다.
리밸런싱 주기가 짧으면 전략이 최신 신호를 빨리 반영한다. 하지만 거래가 잦아져 수수료와 세금 부담이 커진다. 반대로 리밸런싱 주기가 길면 거래비용은 줄지만, 포트폴리오가 목표 비중에서 오래 벗어날 수 있다.
아래 코드는 리밸런싱 날짜마다 목표 비중을 새로 계산하고, 그 비중으로 포트폴리오 수익률을 구하는 단순한 구조다.
def get_rebalance_dates(prices, freq='M'):
periods = prices.index.to_period(freq)
is_last = ~periods.duplicated(keep='last')
return prices.index[is_last]
def backtest_portfolio(prices, weight_func, freq='M', cost=0.001):
returns = prices.pct_change().fillna(0)
weights = pd.DataFrame(index=prices.index, columns=prices.columns, dtype=float)
for date in get_rebalance_dates(prices, freq):
past_prices = prices.loc[:date]
weights.loc[date] = weight_func(past_prices)
weights = weights.ffill().fillna(0)
daily_weights = weights.shift(1).fillna(0)
turnover = weights.diff().abs().sum(axis=1).fillna(0)
portfolio_return = (daily_weights * returns).sum(axis=1) - turnover * cost
return portfolio_return여기서 cost=0.001은 거래비용을 0.1%로 가정한 것이다. 실제 거래에서는 수수료, 세금, 슬리피지를 함께 고려해야 한다. 슬리피지는 내가 기대한 가격과 실제 체결 가격의 차이다.
월간, 분기, 반기 리밸런싱을 비교하려면 다음처럼 실행하면 된다.
monthly = backtest_portfolio(prices, equal_weight, freq='M', cost=0.001)
quarterly = backtest_portfolio(prices, equal_weight, freq='Q', cost=0.001)
half_year = backtest_portfolio(prices, equal_weight, freq='2Q', cost=0.001)
comparison = pd.DataFrame({
'monthly': (1 + monthly).cumprod(),
'quarterly': (1 + quarterly).cumprod(),
'half_year': (1 + half_year).cumprod()
})
comparison.plot(figsize=(12, 5))결과를 볼 때는 수익률만 보면 안 된다. 월간 리밸런싱의 수익률이 조금 높더라도 거래비용이 커지고, 매매가 번거롭다면 실전에서는 분기 리밸런싱이 더 나을 수 있다. 전략은 숫자만으로 선택하지 않는다. 내가 실제로 실행할 수 있어야 한다.
성과 지표로 포트폴리오 평가하기
포트폴리오 백테스트 결과는 최소한 네 가지로 평가해야 한다.
첫째, 누적 수익률이다. 최종적으로 돈이 얼마나 불어났는지 보여준다.
둘째, 변동성이다. 수익률 곡선이 얼마나 흔들렸는지 보여준다.
셋째, MDD다. 고점 대비 최대 낙폭이다. 투자자가 실제로 견뎌야 했던 가장 큰 고통에 가깝다.
넷째, 샤프 비율이다. 감수한 변동성 대비 얼마나 효율적으로 수익을 냈는지 보는 지표다.
def performance_summary(r, rf=0):
r = r.dropna()
total_return = (1 + r).prod() - 1
annual_return = (1 + total_return) ** (252 / len(r)) - 1
annual_vol = r.std() * np.sqrt(252)
wealth = (1 + r).cumprod()
mdd = (wealth / wealth.cummax() - 1).min()
sharpe = np.nan
if annual_vol != 0:
sharpe = (annual_return - rf) / annual_vol
return pd.Series({
'total_return': total_return,
'annual_return': annual_return,
'annual_vol': annual_vol,
'mdd': mdd,
'sharpe': sharpe
})여러 전략을 한 번에 비교할 수도 있다.
strategy_returns = pd.DataFrame({
'equal_monthly': monthly,
'equal_quarterly': quarterly,
'equal_half_year': half_year
})
summary = strategy_returns.apply(performance_summary).T
print(summary)이 표를 보면 어떤 전략이 단순히 많이 벌었는지뿐 아니라, 얼마나 흔들렸고 얼마나 깊게 하락했는지도 함께 볼 수 있다.
벤치마크와 비교하기
전략의 성과는 혼자 보면 착시가 생긴다. 수익률이 30%라고 해도 같은 기간 시장지수가 60% 올랐다면 좋은 전략이라고 말하기 어렵다. 반대로 전략 수익률이 5%에 불과해도 시장이 -20% 하락한 기간이었다면 방어력이 좋은 전략일 수 있다.
그래서 항상 벤치마크와 비교해야 한다. 벤치마크는 비교 기준이다. 한국 주식 전략이라면 KOSPI나 KOSDAQ 지수, 미국 대형주 전략이라면 S&P 500 ETF 같은 것을 사용할 수 있다.
benchmark_return = benchmark_price.pct_change().dropna()
all_returns = pd.concat([
monthly.rename('strategy'),
benchmark_return.rename('benchmark')
], axis=1).dropna()
summary = all_returns.apply(performance_summary).T
print(summary)
(1 + all_returns).cumprod().plot(figsize=(12, 5))벤치마크 비교에서 중요한 질문은 세 가지다.
첫째, 전략이 시장보다 높은 수익률을 냈는가?
둘째, 시장보다 작은 MDD로 버텼는가?
셋째, 높은 수익률이 특정 기간이나 특정 종목 운에 의존한 것은 아닌가?
이 질문에 답하지 못하면 백테스트 결과가 좋아도 실전에 쓰기 어렵다.
좋은 포트폴리오 백테스트의 기준
좋은 포트폴리오 백테스트는 화려한 그래프가 아니다. 다음 조건을 확인할 수 있어야 한다.
어떤 규칙으로 종목을 골랐는가
각 종목의 비중은 어떻게 정했는가
리밸런싱은 언제 했는가
거래비용을 반영했는가
벤치마크와 비교했는가
수익률뿐 아니라 MDD와 샤프 비율을 확인했는가
실제로 매매 가능한 종목과 비중인가
특히 포트폴리오 전략에서는 '수익률이 가장 높았던 조합'을 찾으려는 유혹을 조심해야 한다. 과거 데이터에서 가장 좋아 보이는 종목 수, 리밸런싱 주기, 비중 계산법을 계속 고르면 과최적화에 빠지기 쉽다. 과거에는 완벽했지만 미래에는 약한 전략이 된다.
처음에는 단순한 동일비중 포트폴리오로 시작하자. 그다음 시가총액 비중, 변동성 기반 비중을 비교한다. 마지막으로 월간, 분기, 반기 리밸런싱을 바꿔보며 성과와 거래비용의 균형을 확인한다.
포트폴리오 백테스트의 핵심은 한 종목을 맞히는 능력이 아니다. 여러 실수가 섞여도 전체 전략이 무너지지 않도록 구조를 설계하는 능력이다. 좋은 투자자는 매번 정답 종목을 고르는 사람이 아니라, 틀릴 수 있음을 전제로 계좌를 설계하는 사람이다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
