Intro.
벌써 2025년이다.
적게 일하고 많이 벌고, 투자도 성공하고, 건강하고, 좋은사람도 만나고,
많이 먹고 적게 찌고, 운수가 가득 풀려서 모든게 행복한 한해가 되길 바란당.
(내 나이를 검색하니 만 31세, 닭띠 연나이 32세란다... 오 생각보다 젊잖아?)

최근에 주식 싹 정리하고 리벨런싱을 했다.
리벨런싱을 하다 보니, 포트폴리오 비율대로 뭔가 백테스팅도 돌려보고싶고 해가지구
조금 재밌게 깔끔한 코드르 작성해보려 했다.
다들 주식하는데 리벨런싱하는거 너무 귀찮지 않나?
(토스증권 앱 담당자는 오픈API 제공해서 투자자가 자유롭게 해줄수 있게 하거나, 포트폴리오 리벨런싱 기능 이런거 추가 구현하면 엄청 사랑받을걸요~?) 내 바램이다...
암튼 자! 코드를 작성해보장.
1. 필요 라이브러리 불러오기
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
기본적인 판다 대리고 오고
주가는 yfinance에서 최대한 가지고 오려고한다.
아마 주식가지고 놀아본 사람들은 다 이 라이브러리 사용 한번쯤 해봤을거다.
https://github.com/ranaroussi/yfinance
GitHub - ranaroussi/yfinance: Download market data from Yahoo! Finance's API
Download market data from Yahoo! Finance's API. Contribute to ranaroussi/yfinance development by creating an account on GitHub.
github.com
2. 기본 정보 입력하기
자 이번 목적이 뭔가, 내 주식 포트폴리오를 구성하고 비율도 넣어 봤을때의 기간에 다른 백테스팅 결과다. 추가로 리밸런싱 주기까지 설정할것이다.
그럼 필수적인 요소들이 대충
주식, 주식의 비중, 투자 금액, 리벨런싱 주기, 백테스팅 시작일, 종료일
이 정도면 될것같다.
# 사용자 입력
STOCK_TICKERS = ["TSLA", "NVDA", "PLTR", "VOO", "COST", "XOM", "GLD", "SCHD"]
CRYPTO_TICKERS = ["BTC-USD", "ETH-USD"]
ALL_TICKERS = STOCK_TICKERS + CRYPTO_TICKERS
START_DATE = "2024-01-01"
END_DATE = "2024-12-24"
TARGET_WEIGHTS = {
"TSLA": 0.1,
"NVDA": 0.1,
"PLTR": 0.05,
"VOO": 0.3,
"COST": 0.1,
"XOM": 0.05,
"GLD": 0.05,
"SCHD": 0.2,
"BTC-USD": 0.03,
"ETH-USD": 0.02
}
TOTAL_PORTFOLIO_VALUE = 10000000 # 전체 투자금 (KRW)
REBALANCING_INTERVAL = 90 # 리벨런싱 주기 (일 단위)
위는 뭐 대충 내 포트폴리오와 거의~~~비슷하게 예시로 설정해본거다.
실제로 올해는 주식만 하기에는 암호화폐에 대한 기대가 좀 있어서, 늦었지만 투자 포트폴리오에 추가해볼려고했다. (비중을 낮게)
비중도 대충 설정해주고 금액과 리밸런싱 주기 잘 설정해주자.
3. 과거 데이터 가지고오기
그럼 가장 중요한게 뭐겠나.
주식과 암호화폐의 과거 값들을 가지고 오는거지
def fetch_historical_prices(tickers, start_date, end_date):
all_data = []
for ticker in tickers:
try:
data = yf.download(ticker, start=start_date, end=end_date)
if not data.empty:
data = data['Close'].reset_index()
data.columns = ['Date', ticker]
all_data.append(data)
except Exception as e:
print(f"[오류] {ticker} 데이터를 가져오는 중 문제 발생: {e}")
if all_data:
merged_data = pd.concat(all_data, axis=1)
merged_data = merged_data.loc[:, ~merged_data.columns.duplicated()]
return merged_data
return pd.DataFrame()
- yfinance의 download 함수: 티커별 과거 종가 데이터를 가져오고
- 데이터 병합: 각 티커의 데이터를 pd.concat으로 합치고, 혹시모르니 중복된 Date 열을 제거한다
4. 환율 데이터 반영하기 & 공휴일도 반영하기
이번에 코드 짜면서 상당히 애먹었다...
환율 가지고 오는건 껌이지만, 나중에 수익률 계산하는데 원화로 해야하는데 계속 막 달러로 계산된 값이 나와서 머리 아팠음..
암튼 간단히 환율 데이터를 가지고 오자.
def fetch_exchange_rate(start_date, end_date):
data = yf.download("KRW=X", start=start_date, end=end_date)
if not data.empty:
data = data['Close'].reset_index()
data.columns = ['Date', '환율']
data["환율"] = data["환율"].fillna(method="ffill")
return data
raise ValueError("환율 데이터를 가져올 수 없습니다.")
환율 데이터 역시 yfinance에서 아름답게 가지고 올 수 있댜.
또한 주가 데이터 가지고 오는데 공휴일에 대한 설정도 어느정도 해줘야 애러가 뜨지 않는다, 벡테스팅 날짜 설정하는데 대충 막 공휴일 집어넣으면 위의 주가 데이터에서는 공휴일 정보가 없기때문에 터질 가능성이 많다.
def adjust_dates_for_holidays(df, start_date, end_date):
valid_dates = df['Date'].dropna().sort_values().unique()
adjusted_start = max(pd.Timestamp(start_date), pd.Timestamp(valid_dates[0]))
adjusted_end = min(pd.Timestamp(end_date), pd.Timestamp(valid_dates[-1]))
return adjusted_start.strftime("%Y-%m-%d"), adjusted_end.strftime("%Y-%m-%d")
5. 주가 트랜드 시각화
결과를 데이터 프레임으로 받는건 좋지만, 그래도 내가 고른 주식들이 얼마나 오르는지 시각화 자료 하나 있어도 기분이 좋다 (올라야 기분이 좋다)
def plot_overall_trend(trend_df):
normalized = trend_df.set_index('Date') / trend_df.set_index('Date').iloc[0] * 100
plt.figure(figsize=(12, 6))
plt.plot(normalized, linewidth=2)
plt.title('포트폴리오 전체 트렌드')
plt.xlabel('날짜')
plt.ylabel('정규화된 가격 (%)')
plt.legend(normalized.columns, loc='best')
plt.grid(True)
plt.show()
정규화 잊지말자.
6. 리벨런싱 적용하기
다른 백테스팅에서는 해본적이 없는, 리벨런싱 주기를 조절하여 적용하는 코드가 필요했다.
리벨런싱의 중요성이 얼마나 큰지... 해야 자산 더 오른다..ㅠㅠ
def calculate_rebalancing(trend_df, target_weights, total_portfolio_value, exchange_rate_df, interval):
"""리벨런싱을 적용하여 백테스팅 결과"""
rebalance_dates = trend_df['Date'].iloc[::interval].tolist() # 리벨런싱 날짜 선택
portfolio = {} # 각 티커의 보유 자산
history = [] # 리벨런싱 기록
for i, date in enumerate(trend_df['Date']):
row = trend_df.loc[trend_df['Date'] == date].iloc[0] # 특정 날짜의 가격 데이터
exchange_rate = exchange_rate_df.loc[exchange_rate_df['Date'] == date, '환율'].values[0]
# 리벨런싱일에 포트폴리오 조정
if date in rebalance_dates or i == 0: # 첫 투자 시에도 리벨런싱
portfolio = {
ticker: (total_portfolio_value * weight) / (row[ticker] * exchange_rate if ticker in STOCK_TICKERS else row[ticker])
for ticker, weight in target_weights.items()
if not pd.isna(row[ticker]) # 가격 데이터가 유효한 경우에만 계산
}
# 현재 포트폴리오 가치 계산
current_value = {
ticker: shares * row[ticker] * (exchange_rate if ticker in STOCK_TICKERS else 1)
for ticker, shares in portfolio.items()
if not pd.isna(row[ticker]) # 가격 데이터가 유효한 경우에만 계산
}
# 총 포트폴리오 가치 계산
total_portfolio_value = sum(current_value.values())
# 기록 저장
history.append({
"날짜": date,
"총 포트폴리오 가치 (KRW)": total_portfolio_value,
"총 포트폴리오 가치 (USD)": total_portfolio_value / exchange_rate,
"상세 내역": current_value
})
return pd.DataFrame(history)
- 리벨런싱 날짜 선정
- iloc[::interval]로 사용자가 지정한 리벨런싱 주기에 따라 날짜를 선택하고
- 첫 번째 투자일(i == 0)에도 리벨런싱을 강제로 적용한다
- 목표 비중에 따른 리벨런싱
- 각 자산의 목표 비중(TARGET_WEIGHTS)을 기준으로 자산별 보유량을 계산, 이게 진짜 중요해ㅠ
- 주식은 exchange_rate(환율)를 고려해 원화(KRW) 기준으로, 암호화폐는 USD 기준으로 계산합니다.
- 포트폴리오 가치 계산
- 매일 포트폴리오 내 모든 자산의 현재 가치를 합산해 총 포트폴리오 가치를 계산해줘야하고,
- 환율(exchange_rate)을 반영하여 국내외 자산의 가치를 동일한 기준으로 맞춰야한다
- 리벨런싱 기록
- history 리스트에 날짜별 총 가치, USD로 환산된 총 가치, 각 티커별 현재 가치를 저장ㅎ고
- 최종적으로 DataFrame 형태로 반환한다
7. 최종 계산 결과 반영
위에서 작업했던 모든 함수들을 적용해보자.
def calculate_summary(trend_df, target_weights, total_portfolio_value, exchange_rate_df, interval):
"""리벨런싱 결과를 요약하여 각 종목 및 전체 성과를 계산햐쟈"""
rebalance_results = calculate_rebalancing(trend_df, target_weights, total_portfolio_value, exchange_rate_df, interval)
summary = []
final_exchange_rate = exchange_rate_df.iloc[-1]['환율']
for ticker in target_weights.keys():
if ticker in rebalance_results["상세 내역"].iloc[-1]:
start_value_krw = rebalance_results["상세 내역"].iloc[0].get(ticker, 0)
end_value_krw = rebalance_results["상세 내역"].iloc[-1].get(ticker, 0)
start_value_usd = start_value_krw / final_exchange_rate
end_value_usd = end_value_krw / final_exchange_rate
profit_krw = end_value_krw - start_value_krw
profit_usd = end_value_usd - start_value_usd
profit_percentage = (profit_krw / start_value_krw * 100) if start_value_krw > 0 else 0
summary.append({
"종목": ticker,
"초기 투자금액 (KRW)": start_value_krw,
"초기 투자금액 (USD)": start_value_usd,
"최종 보유금액 (KRW)": end_value_krw,
"최종 보유금액 (USD)": end_value_usd,
"수익 (KRW)": profit_krw,
"수익 (USD)": profit_usd,
"수익률 (%)": profit_percentage
})
total_initial_krw = sum(item["초기 투자금액 (KRW)"] for item in summary)
total_final_krw = sum(item["최종 보유금액 (KRW)"] for item in summary)
total_profit_krw = total_final_krw - total_initial_krw
total_profit_percentage = (total_profit_krw / total_initial_krw * 100) if total_initial_krw > 0 else 0
total_summary = pd.DataFrame({
"항목": ["초기 투자금액 (KRW)", "최종 보유금액 (KRW)", "수익 (KRW)", "수익률 (%)"],
"값": [
total_initial_krw,
total_final_krw,
total_profit_krw,
total_profit_percentage
]
})
return pd.DataFrame(summary), total_summary
- 리벨런싱 결과 가져오기
- 이전에 정의한 calculate_rebalancing 함수로부터 리벨런싱 결과를 가져오고
- rebalance_results DataFrame에서 상세 내역(상세 내역)을 기반으로 개별 종목 성과를 분석한다
- 종목별 성과 계산
- 각 종목에 대해 초기 투자 금액과 최종 보유 금액을 비교해 수익 및 수익률을 계산해줘야한다
- start_value_krw: 초기 투자금 (원화 기준).
- end_value_krw: 최종 보유 금액 (원화 기준).
- profit_percentage: (최종 금액 - 초기 금액) / 초기 금액 × 100.
- 환율 적용
- USD 기준으로도 수익과 수익률을 계산하기 위해 환율(final_exchange_rate)을 적용
- 포트폴리오 전체 성과 계산
- 종목별 초기 투자 금액, 최종 보유 금액을 합산하여 포트폴리오 전체 성과를 요약하는게 필요하당
8. 실행코드
위에서 이쁘게 함수화 시킨걸ㄹ 이제 실행해보자
if __name__ == "__main__":
try:
# 데이터 가져오기
trend_df = fetch_historical_prices(ALL_TICKERS, START_DATE, END_DATE)
exchange_rate_df = fetch_exchange_rate(START_DATE, END_DATE)
if not trend_df.empty:
# 공휴일을 고려하여 날짜 조정
adjusted_start_date, adjusted_end_date = adjust_dates_for_holidays(trend_df, START_DATE, END_DATE)
print(f"조정된 날짜 범위: {adjusted_start_date} ~ {adjusted_end_date}")
# 날짜 필터링
trend_df = trend_df[(trend_df['Date'] >= adjusted_start_date) & (trend_df['Date'] <= adjusted_end_date)]
# 전체 트렌드 시각화
plot_overall_trend(trend_df)
# 백테스팅 요약 계산
summary, total_summary = calculate_summary(
trend_df, TARGET_WEIGHTS, TOTAL_PORTFOLIO_VALUE, exchange_rate_df, REBALANCING_INTERVAL
)
# 결과 출력
print("\n백테스팅 요약:")
print(summary)
print("\n전체 요약:")
print(total_summary)
else:
print("선택한 티커 및 날짜 범위에 대한 유효한 데이터가 없습니다.")
except Exception as e:
print(f"오류 발생: {e}")
- 데이터 가져오기
- fetch_historical_prices를 호출해 주식 및 암호화폐 데이터를 가져오고
- fetch_exchange_rate로 환율 데이터도 챙겨오고
- 날짜 조정 및 필터링
- adjust_dates_for_holidays로 공휴일 등으로 인해 누락된 데이터를 고려하여 분석 범위를 조정하고
- 포트폴리오 트렌드 시각화
- plot_overall_trend을 호출해 데이터 시각화로 전체 트렌드를 확인할거고
- 백테스팅 결과 요약
- calculate_summary로 리벨런싱 결과를 요약하고 종목별 수익률 및 포트폴리오 전체 성과를 계산한댜
- 결과 출력
- 요약된 결과(summary, total_summary)를 콘솔에 출력해서 봐야지
- 에러 처리
- try-except 블록으로 실행 중 발생할 수 있는 예외는 처리하쟈
실행 결과를 보기전에 하나 궁금했던거 집고 넘어갈라고 한다.
아니 왜 맨날 지피티 친구랑 작업할때 마지막 실행할때 저 if__name__ 이게 뭔가 싶었는데...
if __name__ == "__main__":의 의미
모듈로서의 활용과 스크립트 실행 구분:
Python 파일은 직접 실행될 수도 있고, 다른 모듈에서 호출될 수도 있습니다.
if __name__ == "__main__": 블록 안의 코드는 해당 파일이 직접 실행될 때만 실행됩니다.
다른 모듈에서 import한 경우 이 블록은 실행되지 않아, 함수나 클래스만 가져올 수 있습니다.
__name__ 변수의 역할
Python 파일이 실행될 때, 특별한 변수 __name__이 설정됩니다.
파일이 직접 실행되면 __name__의 값은 "__main__"으로 설정됩니다.
파일이 다른 곳에서 import될 경우 __name__은 파일명(모듈명)으로 설정됩니다.
--- GPT
예... 그렇다고 합니다.
암튼요.
결과를 한번 볼까?
9. 결과 및 요약

이야 아름다운 팔란티어 미친놈
시각화 자료는 너무 이쁘고, 데이터 프래임을 봐서 정확한 수익률을 봐보쟈

전체 포트폴리오에서 유일하게 이더리움만 마이너스 수익률을 기록했다.
미친 팔란티어와 테슬라는 뭐... 말할것도없고 24년 모든 주식이 팡팡 잘 올랐기 때문에 예상한 결과다.
전체 포트폴리오의 결과를 한번 봐볼까?

미쳤어요 미쳤어. 70% ㅎㅎㅎㅎ
국장떠나 미장으로 가는 이유가 다 있죠?
(아 물론 팔란티어 테슬라 엔비디아가 너무 많이 해줬다)
이더리움만 마이너스인데... usd로 보면 정말 많이 떨어졋따... (아 환율 뭐냐고, 우리나라 뭐하냐고, 나라꼴진짜..)

위처럼 간단하게 내가 포트폴리오를 구성해보고 비중과 리벨런싱 주기까지 추가했을때의 백테스팅을 해보았다.
나중에는 포트폴리오 비중을 조금 더 심화있게 만드는 방법을 적어볼거다
(몬테카를로로 해야지,,, 예전 기억아 돌아와)
'Data Analysis > 코드 끄적이기' 카테고리의 다른 글
[끄적이기 5] Kaggle: Apple Stock Data and Key Affiliated Companies 애플 주식 데이터 분석 (9) | 2024.10.12 |
---|---|
[끄적이기4] ChatGPT API 활용해서 감성분석해보기 (3) | 2024.10.05 |
[끄적이기3] Kaggle: Movies 1910-2024 (Metacritic) 매타크리틱 영화 데이터 (9) | 2024.09.28 |
[끄적이기2] Selenium THRID_PARTY_NOTICES.chromedriver Error 해결하기 (0) | 2024.07.27 |
[끄적이기1] 구글 플레이스토어, 앱스토어 리뷰정보 크롤링 해보기 (3) | 2024.07.20 |