본문 바로가기

파이썬 & 머신러닝과 딥러닝

주식 포트폴리오 분석을 위한 시뮬레이션 - 몬테카를로 시뮬레이션, 샤프지수, KOSPI 통계분석, 종목간 상관관계, 최대 낙폭, 할로윈 투자 전략

 

주식 포트폴리오 분석을 위한 시뮬레이션

주식 포트폴리오의 수익률과 위험도를 계산하는 과정

이를 기반으로 몬테카를로 시뮬레이션을 통해 다양한 비율로 포트폴리오를 구성하여 수익률과 위험도를 분석

 

몬테카를로 시뮬레이션:

  • 다양한 주식 비중으로 포트폴리오를 구성하고, 그에 따른 예상 수익률과 위험도를 계산
  • 무작위로 비중을 선택하여, 각 포트폴리오가 얼마나 수익을 낼 수 있는지 평가
  • 10,000번 반복하여 다양한 포트폴리오를 테스트

 

pct_change() 함수 : 각 주식의 일간 퍼센트 변동률을 계산

cov() 함수 : 공분산 행렬을 계산하여, 각 주식 간의 변동성을 평가하는 데 사용

 

1. 데이터 불러오기 및 전처리

'stocks.csv' 파일에서 날짜와 주식 종목 데이터를 불러와서 일간 수익률을 계산하는 기반을 마련

import pandas as pd
import numpy as np

# 1. 데이터 불러오기
df = pd.read_csv('stocks.csv')

# 2. 'Unnamed: 0' 컬럼을 날짜 데이터로 변환하고, 이를 인덱스로 설정
df['Unnamed: 0'] = pd.to_datetime(df['Unnamed: 0'])
df = df.set_index('Unnamed: 0')


# 데이터 확인
df.head()  # 상위 5개 행 출력

 

2. 일간 및 연간 수익률 계산

일간 수익률은 주식의 퍼센트 변동률을 계산하여 얻음.

이를 평균내어 1년(252 거래일 기준) 동안의 예상 수익률을 계산

# 3. 일간 수익률 계산 (각 주식의 일간 퍼센트 변화율)
daily_ret = df.pct_change()

# 4. 연간 수익률 계산 (일간 수익률의 평균에 252를 곱하여 연간 수익률로 변환)
annual_ret = daily_ret.mean() * 252

# 연간 수익률 확인
print("연간 수익률:\n", annual_ret)

 

3. 분산(위험도) 및 변동성 계산

일간 수익률의 공분산을 계산하여 일간 위험도를 구하고, 이를 바탕으로 연간 위험도를 계산

이 위험도는 주식의 변동성을 나타냄

# 5. 일간 위험도(분산) 계산
daily_cov = daily_ret.cov()

# 6. 연간 위험도 계산 (일간 공분산을 252로 곱해 연간 위험도로 변환)
annual_cov = daily_cov * 252

# 연간 위험도 확인
print("연간 위험도(공분산):\n", annual_cov)

 

4. 몬테카를로 시뮬레이션

이제 다양한 비율로 주식 포트폴리오를 구성하여, 각 포트폴리오의 수익률과 위험도를 계산

이를 10,000번 반복해서 다양한 포트폴리오의 성과를 시뮬레이션

import numpy as np

box = []
stocks = ['TSLA', 'AAPL', 'NVDA']

# 몬테카를로 시뮬레이션
# 자산의 비중(weights)을 랜덤하게 설정한 후, 이를 바탕으로 포트폴리오의 수익률과 리스크를 계산
for i in range(10000):
    # 자산의 랜덤 비중(weights) 생성 (각 자산에 대한 비율)
    weights = np.random.random(len(stocks))  # 주어진 자산(stocks) 개수만큼 임의의 가중치 생성
    weights /= sum(weights)  # 가중치의 합이 1(즉, 100%)이 되도록 정규화

    # 포트폴리오의 예상 수익률 계산
    # 각 자산의 연간 수익률(annual_ret)과 자산의 비중(weights)을 내적(dot product)하여 포트폴리오의 수익률 계산
    returns = np.dot(weights, annual_ret)

    # 포트폴리오의 예상 리스크(변동성) 계산
    # 가중치(weights)와 연간 공분산 행렬(annual_cov)을 사용하여 리스크 계산
    risk = np.sqrt(np.dot(weights.T, np.dot(annual_cov, weights)))

    # 수익률(returns), 리스크(risk), 자산별 가중치(weights)를 box 리스트에 추가
    # [수익률, 리스크] + [각 자산의 가중치] 형식으로 저장
    box.append([returns, risk] + list(weights))
df = pd.DataFrame(box, columns = ['Returns', 'Risk'] + stocks)
df

 

 

 

import matplotlib.pyplot as plt

df.plot.scatter(x = 'Risk', y = 'Returns', figsize = (10, 5), grid = True)

plt.xlabel('Risk')
plt.ylabel('Returns')
plt.show()

 

같은 리스크라도 Returns 가 높은걸 구매하는게 좋다

→ 샤프 비율 사용


 

샤프 지수(Sharpe Ratio)

투자 성과를 평가하기 위한 지표로, 위험 대비 수익률을 측정

 

샤프 지수 :  (수익률 - 무위험수익률) / 위험도

(은행보단 수익이 더 나와야해서 무위험수익률을 뺌)

 

 

샤프 지수를 포함한 몬테카를로 시뮬레이션

import numpy as np

box = []

stocks = ['TSLA', 'AAPL', 'NVDA']

# 몬테카를로 시뮬레이션
# 자산의 구매 비중에 따라 포트폴리오 수익률, 리스크, 샤프지수를 계산

# 샤프지수: (포트폴리오 수익률 - 무위험 수익률) / 포트폴리오 리스크
for i in range(10000):
    # 자산의 랜덤 비중(weights) 생성
    weights = np.random.random(len(stocks))  # 주어진 자산(stocks) 개수만큼 임의의 가중치 생성
    weights /= sum(weights)  # 가중치의 합이 1(100%)이 되도록 정규화
    
    # 포트폴리오의 예상 수익률 계산
    returns = np.dot(weights, annual_ret)

    # 포트폴리오의 예상 리스크(변동성) 계산
    risk = np.sqrt(np.dot(weights.T, np.dot(annual_cov, weights)))

    # 샤프 지수 계산
    # 무위험 수익률을 사용하여 샤프 지수를 계산 (여기서는 무위험 수익률을 0으로 가정)
    sharpe = returns / risk  # 샤프 지수 = (포트폴리오 수익률) / (포트폴리오 리스크)
    
    # box 리스트에 수익률, 리스크, 샤프 지수, 자산별 비중 추가
    # [수익률, 리스크, 샤프 지수] + [각 자산의 가중치] 형식으로 저장
    box.append([returns, risk, sharpe] + list(weights))

# [수익률, 리스크, 샤프지수] + [각 자산의 가중치] 형식으로 저장
df = pd.DataFrame(box, columns = ['Returns', 'Risk', 'Sharpe'] + stocks)
df

# 최대 샤프 지수의 포트폴리오 정보 찾기
max_sharpe = df[df['Sharpe'] == df['Sharpe'].max()]  # 샤프 지수가 가장 높은 포트폴리오 정보
max_sharpe

# 최소 리스크의 포트폴리오 정보 찾기
min_risk = df[df['Risk'] == df['Risk'].min()]  # 리스크가 가장 낮은 포트폴리오 정보
min_risk  # 가장 안전한 포트폴리오 정보

 

import matplotlib.pyplot as plt

# 리스크(Risk)와 수익률(Returns)을 산점도로 시각화
df.plot.scatter(x='Risk', y='Returns', c='Sharpe', cmap='viridis',
                edgecolor='k', figsize=(10, 5), grid=True)

# 최대 샤프 지수 포트폴리오를 빨간 별표로 표시
plt.scatter(x=max_sharpe['Risk'], y=max_sharpe['Returns'], 
            c='r', marker='*', s=300, label='Max Sharpe')

# 최소 리스크 포트폴리오를 빨간 X로 표시
plt.scatter(x=min_risk['Risk'], y=min_risk['Returns'], 
            c='r', marker='X', s=300, label='Min Risk')

# x축 및 y축 레이블 추가
plt.xlabel('Risk')  # 리스크 레이블
plt.ylabel('Returns')  # 수익률 레이블

# 범례 추가
plt.legend()

# 시각화 출력
plt.show()

→ 최대 샤프 지수 포트폴리오를 빨간 별표로 최소 리스크 포트폴리오를 빨간 X로 표시

 

 


 

종목간 상관관계

import pandas as pd
import matplotlib.pyplot as plt
import FinanceDataReader as fdr

# 1. 데이터 로드 및 전처리
df = pd.read_excel('20210914.xlsx', index_col=0)[['종목명', '종가', '시가총액']]  # Excel 파일에서 종목명, 종가, 시가총액 열을 로드하고 첫 번째 열을 인덱스로 사용
df['비중'] = df['시가총액'] / df['시가총액'].sum() * 100  # 각 종목의 시가총액 비중 계산
df.sort_values('비중', ascending=False)  # 비중 기준으로 데이터프레임을 내림차순 정렬

# KOSPI와 삼성전자의 관계를 분석하기 위한 주석
# KOSPI = 삼성전자? 

# 2. KOSPI 및 삼성전자 주가 데이터 가져오기
kospi = fdr.DataReader('KOSPI')  # KOSPI 지수 데이터를 가져옴
samsung = fdr.DataReader('005930')  # 삼성전자 주가 데이터를 가져옴

# 3. KOSPI와 삼성전자의 종가 데이터 결합
data = [kospi['Close'], samsung['Close']]  # KOSPI와 삼성전자의 종가 데이터를 리스트로 만듦
df = pd.concat(data, axis=1, keys=['kospi', 'samsung']).dropna()  # 데이터를 데이터프레임으로 결합하고 결측치 제거

# 4. KOSPI와 삼성전자의 산점도 그리기
df.plot.scatter(x='samsung', y='kospi')  # 삼성전자의 종가를 x축, KOSPI 종가를 y축으로 산점도 그리기
plt.title('Samsung vs KOSPI Scatter Plot')  # 산점도 제목 추가
plt.xlabel('Samsung Price')  # x축 레이블 추가
plt.ylabel('KOSPI Price')  # y축 레이블 추가
plt.grid()  # 그리드 추가
plt.show()  # 산점도 그래프 출력

# 5. KOSPI와 삼성전자 주가의 비율 계산
A = df['kospi'] / df['kospi'].iloc[0]  # KOSPI 주가의 비율 계산 (첫 날 주가 기준)
B = df['samsung'] / df['samsung'].iloc[0]  # 삼성전자 주가의 비율 계산 (첫 날 주가 기준)

# 6. KOSPI와 삼성전자의 비율 시각화
plt.plot(A)  # KOSPI 비율 그래프
plt.plot(B)  # 삼성전자 비율 그래프
plt.legend(['KOSPI', 'Samsung'])  # 범례 추가
plt.grid()  # 그리드 추가
plt.title('Price Ratio Over Time')  # 비율 그래프 제목 추가
plt.xlabel('Days')  # x축 레이블 추가
plt.ylabel('Price Ratio')  # y축 레이블 추가
plt.show()  # 비율 그래프 출력

 


 

KOSPI 종목 중 SK하이닉스와 가장 상관관계가 높은 종목과 낮은 종목을 찾아보기

import os  # 운영 체제와 상호작용하기 위한 모듈
import pandas as pd  # 데이터 처리 및 분석을 위한 pandas 라이브러리

box = []  # 상관계수를 저장할 리스트 초기화

# SK하이닉스 주식 데이터 불러오기
sk = pd.read_csv("KOSPI/SK하이닉스.csv")  # CSV 파일에서 SK하이닉스 주식 데이터 읽기
sk['Date'] = pd.to_datetime(sk['Date'])  # 'Date' 컬럼을 datetime 형식으로 변환
sk = sk.set_index("Date")  # 'Date' 컬럼을 인덱스로 설정
sk = sk['Close']  # 'Close' 컬럼만 선택하여 변수 sk에 저장

# KOSPI 폴더 내의 모든 파일에 대해 반복
for i in os.listdir("KOSPI"):  # KOSPI 폴더 내 파일 리스트 가져오기
    name = i.split('.')[0]  # 파일명에서 확장자를 제거하여 주식명 저장
    df = pd.read_csv("KOSPI/" + i)  # CSV 파일에서 현재 주식 데이터 읽기
    df['Date'] = pd.to_datetime(df['Date'])  # 'Date' 컬럼을 datetime 형식으로 변환
    df = df.set_index("Date")  # 'Date' 컬럼을 인덱스로 설정
    df = df['Close']  # 'Close' 컬럼만 선택하여 변수 df에 저장
    
    # SK하이닉스와 현재 주식 데이터를 열 방향으로 결합
    a = pd.concat([sk, df], axis=1, keys=['A', 'B'])  
    # 두 데이터프레임의 상관계수 계산 (절대값)
    corr = abs(a.corr().iloc[0, 1])  
    # 상관계수와 주식명 리스트에 추가
    box.append([name, corr])  

# 상관계수를 데이터프레임으로 변환하고 Corr 컬럼 기준으로 정렬
result = pd.DataFrame(box, columns=['Name', 'Corr']).sort_values('Corr')  

# 상관계수가 가장 낮은 주식 정보 출력
print(result.iloc[0])  
print()  # 빈 줄 출력

# 상관계수가 두 번째로 높은 주식 정보 출력
print(result.iloc[-2])

 


 

KOSPI(한국 종합주가지수)에 대한 다양한 통계 분석

import FinanceDataReader as fdr
import pandas as pd

# KOSPI 데이터 가져오기
kospi = fdr.DataReader('KOSPI')

# KOSPI 데이터 확인
print(kospi)

# KOSPI 종가의 최대값과 최소값 출력
max_close = kospi['Close'].max()  # 최대 종가
min_close = kospi['Close'].min()  # 최소 종가
print(max_close, min_close)

# 최대 종가의 날짜 찾기
date_max = kospi['Close'].idxmax()  # 최대 종가가 발생한 날짜
print(date_max)

# 최소 종가의 날짜 찾기
date_min = kospi['Close'].idxmin()  # 최소 종가가 발생한 날짜
print(date_min)

# 변동폭 계산 (현재 종가 - 이전 종가)
kospi['변동폭'] = kospi['Close'] - kospi['Close'].shift(1)

# 변동폭이 가장 큰 5일 데이터 정렬 후 상위 5개 출력
top_5_volatility = kospi.sort_values('변동폭', ascending=False).head(5)
print(top_5_volatility)

# 변동폭이 가장 작은 5일 데이터 출력
smallest_5_volatility = kospi['변동폭'].nsmallest(5)
print(smallest_5_volatility)

# KOSPI 데이터를 리셋하여 새로운 DataFrame 생성
kospi2 = kospi.reset_index()
print(kospi2.head())

# KOSPI 월별 정리 - 각 월의 첫 시가, 마지막 종가, 최고가, 최저가, 거래량 총합 계산
how = {
    'Open': 'first',  # 월의 첫 거래일 시가
    'Close': 'last',  # 월의 마지막 거래일 종가
    'High': 'max',    # 월의 최고가
    'Low': 'min',     # 월의 최저가
    'Volume': 'sum'   # 월의 거래량 총합
}
monthly_summary = kospi2.groupby(pd.Grouper(key='Date', freq='MS')).agg(how)  # 월별 그룹화 및 집계 수행
print(monthly_summary)

# KOSPI 지수가 100 포인트를 달성했을 때의 날짜와 지수 찾기
box = []
for i in range(400, 3400, 100):  # 400 포인트부터 3400 포인트까지 100 단위로 반복
    cond = kospi['Close'] >= i  # 현재 지수가 i 이상인 조건
    if not cond.empty:  # 조건이 존재할 경우
        box.append([f'{i} 포인트 돌파!',  # 이벤트 메시지
                     kospi.index[cond][0],  # 해당 포인트를 처음 달성한 날짜
                     kospi.loc[cond, 'Close'].iloc[0]])  # 해당 날짜의 지수

# 이벤트 결과를 DataFrame으로 변환하여 출력
event_summary = pd.DataFrame(box, columns=['이벤트', '일자', '지수'])
print(event_summary)

 

KOSPI 지수에서 "포인트" :  KOSPI 지수의 수준

KOSPI 지수는 주식 시장의 전체적인 상태를 나타내는 수치로, 보통 포인트라는 단위로 표시

예를 들어, KOSPI 지수가 2400포인트라면 KOSPI의 지수 값이 2400을 의미하는 것이고,

이 값이 상승하거나 하락할 때마다 그 변동을 포인트 단위로 표현

 


 

KOSPI 기간 수익률

: KOSPI 지수의 처음과 마지막 종가를 비교하여 기간 동안 얼마나 상승했는지 백분율로 계산

rate = kospi.iloc[-1, 3] / kospi.iloc[0, 3]  # 마지막 행의 4번째 열(종가) / 첫 번째 행의 4번째 열(종가)
rate = (rate - 1) * 100  # 기간 수익률을 백분율로 변환
rate

 

연 복리 수익률 (CAGR)

: 특정 기간 동안의 평균 연간 성장률을 측정하는 지표

diff = kospi.index[-1] - kospi.index[0]  # 기간 계산 (마지막 날짜 - 첫 번째 날짜)
year = diff.days / 365  # 기간을 연도로 환산

cagr = rate ** (1/year) - 1  # 연 복리 수익률 계산
cagr

 

 


 

최대 낙폭(MDD)

삼성전자(종목 코드: 005930)의 최대 낙폭(MDD, Maximum Drawdown)을 계산하고 시각화

 

최대 낙폭(MDD)이란?

  • 낙폭(Drawdown)은 특정 시점에서 주가가 이전 최고점에서 얼마나 하락했는지를 백분율로 나타냄.
  • 최대 낙폭(MDD)은 투자 기간 중 가장 큰 낙폭을 의미(리스크를 평가하는 중요한 지표)
kospi = fdr.DataReader('005930')  # 삼성전자 데이터를 불러옴
kospi['전고점'] = kospi['Close'].cummax()  # 누적된 고점을 계산

# 낙폭 계산
kospi['DD'] = (1 - kospi['Close'] / kospi['전고점']) * 100  # 낙폭을 백분율로 계산

(1 - kospi['Close'] / kospi['전고점']) * 100: 주가가 전고점 대비 몇 퍼센트 하락했는지 계산

 

MDD = kospi['DD'].max()  # 낙폭 중 가장 큰 낙폭 (최대 낙폭)
MDD

 

fig = plt.figure(figsize = (15, 5))

ax1 = fig.add_subplot(2, 1, 1)
ax2 = fig.add_subplot(2, 1, 2)

# 주가와 낙폭을 각각 그리기
ax1.plot(kospi.index, kospi['Close'], label = 'Close')  # 주가(종가) 그래프
ax2.plot(kospi.index, kospi['DD'] * -1, label = 'Drawdown')  # 낙폭 그래프 (음수로 그려짐)
ax2.fill_between(kospi.index, kospi['DD'] * -1, alpha = 0.5)  # 낙폭 영역을 음영 처리

# 그리드와 범례 추가
ax1.grid()
ax2.grid()
ax1.legend()
ax2.legend()

# 그래프 표시
plt.show()

 


 

할로윈 투자 전략

: 11월에 매수하고 다음 해 4월에 매도하는 전략

 

할로윈 투자 전략을 백테스팅하는 과정과 매월 투자했을 경우의 누적 수익률을 계산하는 과정

 

1. 누적 수익률 계산

2001년부터 2023년까지 매년 11월에 매수하고 다음 해 4월에 매도하는 전략으로 누적 수익률을 계산

# 할로윈 투자 전략 백테스팅 (2001 ~ 2023년) : 11월에 매수하고 다음 해 4월에 매도하는 전략
cum_rate = 1  # 누적 수익률을 1로 초기화
for year in range(2001, 2024):  # 2001년부터 2023년까지 반복
    buy_mon = str(year) + '-11'  # 매수 시점: 해당 연도의 11월
    sell_mon = str(year + 1) + '-04'  # 매도 시점: 다음 연도의 4월
    buy_price = kospi.loc[buy_mon].iloc[0]['Open']  # 해당 11월의 첫 번째 영업일의 시가 매수
    sell_price = kospi.loc[sell_mon].iloc[-1]['Close']  # 해당 4월의 마지막 영업일의 종가 매도
    rate = sell_price / buy_price  # 해당 기간의 수익률 계산
    cum_rate = cum_rate * rate  # 누적 수익률 계산

# 최종 누적 수익률 출력
cum_rate  # 약 18배의 누적 수익률

→ 누적 수익률 약 18배

 

2. 연평균 수익률(CAGR) 계산

# 연평균 수익률(CAGR) 계산 (연 복리 수익률)
CAGR = (cum_rate ** (1/24)) - 1  # 24년 기간 동안의 연평균 수익률 계산
CAGR  # 연평균 수익률 출력

→  24년 기간 동안 매년 발생한 평균적인 수익률 12배

 

3. 단순 보유 시 수익률 비교

# 단순 보유했을 때의 누적 수익률 계산
simple_rate = kospi.iloc[-1]['Close'] / kospi.iloc[0]['Open']  # 처음부터 끝까지 단순히 주식을 보유했을 때의 누적 수익률
simple_rate  # 약 8배

→ 같은 기간 동안 주식을 단순히 보유했을 경우의 누적 수익률은 약 8배

 

# 단순 보유 시의 연평균 수익률 계산
simple_rate ** (1/24) - 1  # 24년간 단순 보유했을 때의 연평균 수익률
# 연평균 수익률 약 9%

 

3. 정말로 11월에 사면 좋은지?

# 정말로 11월에 사는 것이 더 좋은지 확인하기 위해 각 달에 매수했을 경우 비교
import datetime
from dateutil.relativedelta import relativedelta

# 시작 날짜를 2001년 11월 1일로 설정
start = datetime.datetime(year = 2001, month = 11, day = 1)
# 시작 날짜로부터 5개월 후인 4월로 설정
end = start + relativedelta(month = 5)
start.strftime('%Y - %m')  # 날짜 형식 출력
# 6개월 주기로 매수, 매도하는 수익률을 계산하는 함수
def month6(df, start_year=2001, end_year=2021, month=11):
    cum_rate = 1  # 누적 수익률 초기화
    for year in range(start_year, end_year+1):  # 주어진 기간 동안 매년 반복
        start = datetime.datetime(year=year, month=month, day=1)  # 매수 시작 시점
        end = start + relativedelta(months=5)  # 매도 시점은 매수 시점으로부터 5개월 후
        buy_mon = start.strftime("%Y-%m")  # 매수 시점(연도-월)
        sell_mon = end.strftime("%Y-%m")  # 매도 시점(연도-월)
        
        # 매수 시점의 첫 번째 영업일 시가
        buy_price = df.loc[buy_mon].iloc[0]['Open']
        # 매도 시점의 마지막 영업일 종가
        sell_price = df.loc[sell_mon].iloc[-1]['Close']
        
        rate = sell_price / buy_price  # 해당 기간 수익률 계산
        cum_rate = cum_rate * rate  # 누적 수익률 계산

    return cum_rate  # 최종 누적 수익률 반환
# 매달 투자했을 때의 누적 수익률을 계산하는 코드
data = {}
for month in range(1, 13):  # 1월부터 12월까지 모든 달에 대해 반복
    ret = month6(kospi, 2001, 2021, month)  # 각 달에 매수했을 경우의 누적 수익률 계산
    print(month, ret)  # 해당 달의 수익률 출력
    data[month] = ret  # 결과를 딕셔너리에 저장

 

# 11월에 투자했을 때의 수익률 계산
month6(kospi, 2001, 2023, 11)

 

# 월별 누적 수익률을 시각화 (막대 그래프 그리기)
plt.bar(data.keys(), data.values())  # 월별로 수익률을 막대로 표현
plt.xlabel('Month')  # x축 라벨: Month (월)
plt.ylabel('Return')  # y축 라벨: Return (수익률)
plt.xticks([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])  # x축 값 설정 (1월~12월)
plt.grid(axis='y')  # y축에 그리드 추가
plt.show()  # 그래프 출력