영구 포트폴리오 직장인에게 최적화하기 (2)
이번 글은 지난 글에 이어서 직장인에 맞는 적금식 영구 포트폴리오의 운용과 조금이라도 나은 수익률을 위한 운용 전략을 시뮬레이션을 통하여 찾아보고 그 전략을 공유하는 글이다. 먼저 나는 매월 발생하는 투자금액을 균등하게 나누어서 무조건 자산을 구매하는 전략을 취했을 때 연평균 6.14%의 복리 이자의 효과를 거둘 수 있음을 확인했다. 이번에는 자산 가격의 변화에 따라 분할 매수/분할 매도를 했을 때 수익률이 어떻게 달라질지를 분석하였다.
전략 2. 각 자산 가격의 변동성을 보고 분할 매수/매도 하기
이 전략의 투자금액은 전략 1과 똑같이 매월 80만 원 기준으로 20만 원씩 분배한다. 그러나 각 자산별로 1) 수익 상황에 따라(수익/손실) 혹은 자산 가격의 변동성에 따라(가격의 상승/하락) 분할 매수/매도를 하여 이익을 극대화하는 전략이다. 이를 더 자세하게 상황별로 정리하면 아래와 같다.
수익을 내고 있을 때
- 자산 가격이 A% 이상 상승한 경우 보유 주식의 B%만큼 분할 매도 (수익 실현)
- 자산 가격이 C% 이상 하락한 경우 보유 주식의 D%만큼 분할 매도 (수익 실현)
- 그 외, 보유 예수금의 E%만큼 분할 매수
손실을 보고 있을 때
- 자산 가격이 F% 이상 하락한 경우 보유 주식의 G%만큼 분할 매도 (손실 최소화)
- 그 외, 보유 예수금의 H%만큼 분할 매수
전략을 단순화하기 위하여 가격의 변화는 최근 1달 동안의 변화만 참고하기로 하였고 각 A~H의 변수들에 대해서는 20000번의 Random Search 방식을 통하여 최고 수익금을 발생시키는 변수들을 선택하였다.
백테스트 결과
백테스트 결과에 따르면 2005년부터 2020년 1월까지 매 월 80만 원씩 1억 5040만 원의 투자금액을 기준으로 전략 2번을 사용했을 때 약 2억 8190만 원으로 2억 4835만 원의 전략 1을 약 3355만 원 차이로 앞섰다. 이는 15년간 87.4%의 수익률로 이 전략의 수익률을 예금이자로 계산하면 7.63%로 계산된다. 또한 MDD 값도 2013년 12월 1일에 무려 6.3%로 전략 1의 10.9% 보다 훨씬 방어력이 높은 포트폴리오 운용 전략이 완성되었다. 물론 hyperparameter(A-H)를 과거 데이터에 과적합시킨 결과이기 때문에 완벽하게 신뢰를 하면 안 되지만 전략 1의 underfitting 함도 무시할 수 없기 때문에 일정 조건에 다다랐을 때 분할 매수/매도하는 전략이 더 좋은 수익을 낼 확률이 높다고 정리할 수 있다. 계산된 hyperparameter의 결과를 정리하면 아래와 같다.
주식 운용 전략
수익을 내고 있을 때
- 자산 가격이 10.8%이상 상승한 경우 보유 주식의 23.6%만큼 분할 매도 (수익 실현)
- 자산 가격이 8.3%이상 하락한 경우 보유 주식의 19.6%만큼 분할 매도 (수익 실현)
- 그 외, 보유 예수금의 76%만큼 분할 매수
손실을 보고 있을 때
- 자산 가격이 4.9%이상 하락한 경우 보유 주식의 30.7%만큼 분할 매도 (손실 최소화)
- 그 외, 보유 예수금의 55.3%만큼 분할 매수
채권 운용 전략
수익을 내고 있을 때
- 자산 가격이 29.3%이상 상승한 경우 보유 주식의 48.4%만큼 분할 매도 (수익 실현)
- 자산 가격이 7.9%이상 하락한 경우 보유 주식의 19.8%만큼 분할 매도 (수익 실현)
- 그 외, 보유 예수금의 29.7%만큼 분할 매수
손실을 보고 있을 때
- 자산 가격이 12.2%이상 하락한 경우 보유 주식의 58.8%만큼 분할 매도 (손실 최소화)
- 그 외, 보유 예수금의 17%만큼 분할 매수
금 운용 전략
수익을 내고 있을 때
- 자산 가격이 24%이상 상승한 경우 보유 주식의 36%만큼 분할 매도 (수익 실현)
- 자산 가격이 1%이상 하락한 경우 보유 주식의 63%만큼 분할 매도 (수익 실현)
- 그 외, 보유 예수금의 81.9%만큼 분할 매수
손실을 보고 있을 때
- 자산 가격이 3%이상 하락한 경우 보유 주식의 20.2%만큼 분할 매도 (손실 최소화)
- 그 외, 보유 예수금의 86.9%만큼 분할 매수
주식은 조금이라도 변화 폭이 커지면 3분의 1 정도를 분할 매수/매도하고 변화의 폭이 작아지면 대부분의 예수금을 다시 투자에 활용하는 것이 좋다. 그에 비해 채권은 꽤 많은 양의 수익이 날 때까지도 계속 매수를 해도 괜찮은 모습을 보이고 대신 손실을 보고 있을 때는 빠르게 대부분의 돈을 청산하고 다시 매수를 할 때도 적당한 비율로 조금씩 매수하는 것이 좋다. 금은 오히려 상승에는 둔감하게 반응했지만 하락에는 민감하게 반응하여 파는 것이 좋다는 결과가 나왔다. 매번 시뮬레이션을 돌릴 때마다 최적의 변수는 다양하게 발생할 수 있기 때문에 기호에 맞게 선택해서 쓸 수도 있을 것 같다.
구현
기존의 utils.py 파일을 그대로 사용하고 simulation_strategy.py 파일을 추가하였다.
import random
from utils import (
load_stock_prices, to_kr_prices, get_dates, save_results, get_mdd
)
# 가격의 최대 변화폭 계산
def obtain_max_diffs(prices):
prev_price = prices[0]
max_increment = 0
max_decrement = 0
for price in prices[1:]:
price_diff = (price - prev_price) / prev_price
if price_diff > max_increment:
max_increment = price_diff
if -price_diff > max_decrement:
max_decrement = -price_diff
prev_price = price
return [max_decrement, max_increment]
# 예금에대한 처리를 위한 helper function
def obtain_cash_results(duration, price = 200000):
results = []
cash = price
results.append(cash)
for _ in range(1, duration):
cash *= 1.00165
cash += price
results.append(int(cash))
return results
# 정해진 hyperparameter에 대한 시뮬레이션 진행
def simulate(prices, a, b, c, d, e, f, g, h):
prev_price = prices[0]
volume = 200000 // prev_price
balance = volume * prev_price
deposit = 200000 - balance
buying_balance = balance
results = [deposit + balance]
for price in prices[1:]:
deposit += 200000
price_diff = (price - prev_price) / prev_price
updated_balance = price * volume
# 수익을 보고 있을 때
if buying_balance < updated_balance:
# 자산 가격이 A% 이상 상승한 경우 보유 주식의 B%만큼 분할 매도 (수익 실현)
if price_diff > a:
sell_volume = int(volume * b)
deposit += sell_volume * price
buying_balance -= sell_volume * price
volume -= sell_volume
balance = volume * price
# 자산 가격이 C% 이상 하락한 경우 보유 주식의 D%만큼 분할 매도 (수익 실현)
elif price_diff < -c:
sell_volume = int(volume * d)
deposit += sell_volume * price
buying_balance -= sell_volume * price
volume -= sell_volume
balance = volume * price
# 그 외, 보유 예수금의 E%만큼 분할 매수
else:
buy_volume = int(deposit * e) // (price * 1.003)
deposit -= int(buy_volume * price * 1.003)
buying_balance += buy_volume * price
volume += buy_volume
balance = volume * price
# 손실을 보고 있을 때
else:
# 자산 가격이 F% 이상 하락한 경우 보유 주식의 G%만큼 분할 매도 (손실 최소화)
if price_diff < -f:
sell_volume = int(volume * g)
deposit += sell_volume * price
buying_balance -= sell_volume * price
volume -= sell_volume
balance = volume * price
# 그 외, 보유 예수금의 H%만큼 분할 매수
else:
buy_volume = int(deposit * h) // (price * 1.003)
deposit -= int(buy_volume * price * 1.003)
buying_balance += buy_volume * price
volume += buy_volume
balance = volume * price
prev_price = price
results.append(int(deposit + balance))
return results
# Hyperparameter 탐색을 위하여 각 자산별로 20000번의 시뮬레이션 실행
def search(prices):
best_score = -1
best_results = None
max_decrement, max_increment = obtain_max_diffs(prices)
for _ in range(20000):
a = random.random() * max_increment
b = random.random()
c = random.random() * max_decrement
d = random.random()
e = random.random()
f = random.random() * max_decrement
g = random.random()
h = random.random()
results = simulate(prices, a, b, c, d, e, f, g, h)
if best_score < results[-1]:
best_score = results[-1]
best_results = results
return best_results, (a, b, c, d, e, f, g, h)
# 월별 데이터 전처리
dates = get_dates()
dollar_prices = load_stock_prices('USD_KRW')
stock_prices = to_kr_prices(load_stock_prices('SPY'), dollar_prices)
bond_prices = to_kr_prices(load_stock_prices('TLT'), dollar_prices)
gold_prices = to_kr_prices(load_stock_prices('IAU'), dollar_prices)
# 시뮬레이션 실행
stock_results, stock_params = search(stock_prices)
bond_results, bond_params = search(bond_prices)
gold_results, gold_params = search(gold_prices)
cash_results = obtain_cash_results(len(dates))
only_cash_results = obtain_cash_results(len(dates), price=800000)
print('Stock params', *[f'{p:.3f}' for p in stock_params])
print('Bond params', *[f'{p:.3f}' for p in bond_params])
print('Gold params', *[f'{p:.3f}' for p in gold_params])
total_results = zip(stock_results, bond_results, gold_results, cash_results)
total_results = [sum([sr, br, gr, cr]) for sr, br, gr, cr in total_results]
mdd, mdd_date = get_mdd(dates, total_results)
print('MDD', mdd, mdd_date)
print('Result', total_results[-1])
inputs = list(range(800000, 800000 * (len(dates) + 1), 800000))
total_results = zip(dates, inputs, total_results, only_cash_results)
save_results(total_results)
다음에 탐색할 전략 계획
보통 영구 포트폴리오 같이 정해진 비율로 자산을 가지고 있는 자산 운용 전략은 수익이 나서 비중이 높아진 자산을 팔고 비중이 적어진 자산을 사서 (rebalancing) 더 좋은 수익을 낸다. 그렇다면 매달 투자 금액이 적금식으로 추가될 때 현재 자산들의 비중에 따라서 투자 금액을 분배하여 매수하면 전략 1과 비교해서 어떻게 수익금이 달라질지 분석해 볼 것이다.