아래 3개는 순서대로 정규분포, 라플라스분포, T분포를 데이터에 fitting한 것입니다.
마지막은 실제 주가분포와 각각의 확률분포 실험을 비교했어요. 결과는..재밌네요! 다시 코드로 돌아가죠.
# imaginary daily price return distribution by Normal distribution
norm_fit_param = norm.fit(log_ret_SPY_close)
print(f'Norm dist fit param: {norm_fit_param}')
img_log_ret0 = norm.rvs(*norm_fit_param, size=count_bday, random_state=None)
print(f'Norm rvs: {img_log_ret0}')
ax2.hist(img_log_ret0, density=True, histtype='stepfilled', alpha=0.5, bins=bin_div10)
# ax2.legend(loc='best', frameon=False)
이제 본격적으로 확률분포를 위 데이터에 fitting시켜봅니다.
정규분포(normal distribution) 먼저 해보죠.
scipy.stats.norm에 내장된 fit 메서드를 사용해서 수익률 데이터를 넣고, 결과를 norm_fit_param 변수에 받아옵니다. 정규분포의 경우 loc(평균)과 scale(분산) 순서로 튜플로 반환할 것입니다.
그리고 해당 확률분포를 따르는 난수를 만들어봅시다. norm.rvs는 주어진 param을 따르는 난수를 생성해주는 메서드입니다. size에 넣는 수 만큼 list에 원소로 집어넣어서 반환해줘요. 지금은 SPY 거래일 수 만큼 난수를 생성해봅시다!
rvs 메서드로 만든 상상의 수익률을 히스토그램으로 plot합니다.
결과는 위에 그림 참고~
# imaginary daily price return distribution by Laplace distribution
laplace_fit_param = laplace.fit(log_ret_SPY_close)
print(f'Laplace dist fit param: {laplace_fit_param}')
img_log_ret1 = laplace.rvs(*laplace_fit_param, size=count_bday, random_state=None)
print(f'Laplace rvs: {img_log_ret1}')
ax3.hist(img_log_ret1, density=True, histtype='stepfilled', alpha=0.5, bins=bin_div10)
# ax3.legend(loc='best', frameon=False)
라플라스분포에 대해서도 똑같이 해줘요. 라플라스 분포도 인자가 loc과 scale 두개이므로 fitting parameter를 튜플로 반환합니다.
# imaginary daily price return distribution by Student's T distribution
t_fit_param = t.fit(log_ret_SPY_close)
print(f'T dist fit param: {t_fit_param}')
img_log_ret2 = t.rvs(*t_fit_param, size=count_bday, random_state=None)
print(f'T rvs: {img_log_ret2}')
ax4.hist(img_log_ret2, density=True, histtype='stepfilled', alpha=0.5, bins=bin_div10)
# ax4.legend(loc='best', frameon=False)
마지막으로 Student's T분포입니다. 정규분포랑 대표적으로 비교되는 확률변수인데, Fat-tail이 두드러지는 확률모델이에요.
S&P500 수익률과 확률분포 비교
사실 정규분포는 꼬리쪽 분포에 사건이 발생할 확률이 매우 낮습니다. Fat-tail 사건(하루만에 폭락, 폭등)에 대한 설명이 너무 설득력이 없는데 주식판에서는 정말 흔하게 발생하는 일입니다. 예를 들면 1929 대공황, 1987 블랙 먼데이, 2000년 닷컴버블, 2008 세계금융위기, 그리고 2020 코로나 붕괴까지..
그리고 평범한 날들은 수익률이 0에 가깝게 움직이는 경우가 훨씬 많았어요.
그림만 봐도.. 주황색 선은 실제 수익률에 잘 fitting이 되지 않아요.
대신 Student's T 분포나 Laplace분포를 이용해 피팅하면 훨씬 설득력있는 확률모형을 만들 수 있을것 같습니다.
Student's T분포는 특성상 fat-tail이 나름 설명돼요. 근데 0에 가까운 움직임은 약간 모자란게 조금 아쉽습니다.
바로 이전 글에서 어떤 API를 사용해서 증권 가격 정보를 파이썬으로 받아올 까 고민을 했었다.
이런 고민의 배경에는 미래의 공부를 위해 수정 종가(adjusted close price) 정보 뿐 아니라, 흔히 OHLC라고 부르는 시고저종가의 수정가격을 전부 알고 싶었기 때문이다.
과거의 가격 정보를 가지고 데이터를 뽑아낼 때는 반드시 수정 가격을 사용해야한다. 그렇지 않으면 이런 사태가 벌어진다.
Apple(APPL) historical close price
위는 애플의 10년 종가 차트다. 2014년 6월 9일 애플의 주가가 한순간에 떡락을 해버린걸까?... 당연히 아니다. 그 날 너무 비싼 주당 가격을 1대 7로 액면분할 했을 뿐이다. 그 사실은 애플의 투자자를 위한 공식 자료에서도 볼 수 있다. (링크)
수정 가격을 사용하면 이런 과거의 절대 가격도 현재 가격의 시점으로 볼 수 있게 된다.
Apple(APPL) historical adjusted close price
애플의 위엄이 이제야 제대로 보인다. 현재 시점으로 과거 가격에다 과거 발생한 액면분할(stock split)과 현금배당(cash dividends)을 반영한 가격이 수정가격(adjusted price)이라고 보면 된다.
안타깝게도 모든 수정 OHLC 가격을 그냥 뿌려주는 API는 없었다. 그래서 희망을 가져본 것이 FinanceDataReader 패키지였다. 잘은 몰라도 investing.com 등에서 이미 수정된 가격을 가져오는 것 같았다. 그런데 아무리 봐도 아쉬운 점이 있었으니, 과거 데이터로 갈수록 실제 수정 가격이랑 약간 오차가 있었다.*
*오차가 있다고 스스로 결론지었지만, 잘못된 정보를 드렸을 가능성이 있으므로 여러분도 직접 계산을 해보길 바랍니다.
import datetime as dt
from dateutil.relativedelta import relativedelta
import pandas_datareader as web
import numpy as np
from pandas import DataFrame, Series
import pandas as pd
%matplotlib inline
import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = (14,8)
plt.rcParams['font.size'] = 16
plt.rcParams['lines.linewidth'] = 1
plt.rcParams["axes.grid"] = True
plt.rcParams['axes.axisbelow'] = True
import mplfinance as mpf
먼저 패키지들을 import해오자.
datetime 및 relativedelta는 정확히 현재 시점으로 과거 10년의 데이터를 가져오기 위해서 가격 정보 수집을 위해서 pandas_datareader를, 벡터 연산을 위해 numpy를, 가격정보를 담을 자료구조를 위해 pandas DataFrame을, 차트를 그리기 위해 matplotlib과 mplfinance를 import했다.
plt.rcParams...부분은 앞으로 그릴 플롯의 형태를 미리 전역 설정해주는것이다. 자세한건 나중에 따로 공부해야지.
10년 전 오늘의 날짜를 정확하게 가져오려면 이러한 방식을 사용해야 한다. 이해가 안 가는 것이 있다면 datetime 패키지의 timedelta 클래스를 사용해서는 1년 이상의 날짜 연산이 안된다. timeutil의 relativedelta 클래스를 사용해줌으로써 해결.
중요한 부분이다. 애플의 10년 가격 정보와 수정 정보를 받아오는것인데, 그 전에 먼저 alpha-vantage(이하 av)에 무료 회원가입을 하고서 API 키를 받아야한다(링크). 가입하면서 알려주는 API 키는 한번 창을 끄면 다시는 알려주지 않으므로 반드시 따로 저장해놓도록 한다. 이렇게 받은 API키는 av_apikey 변수에 string으로 할당해놓자. pandas_datareader의 DataReader 메서드를 통해 av API를 사용할 수 있다. 첫 번째 패러미터로 티커를, 두번째는 'av-daily-adjusted'를 입력해줌으로써 av의 일간 수정 가격을 받아오겠다고 명시하자. 세 번째에는 아까 받은 API key를 꼭 제공해야 한다.
그리고 약간의 가공을 해주자. av에서는 데이터의 인덱스(날짜)를 string type으로 주는데, 호환성 높은 시계열 데이터로 만들기 위해서는 to_datetime을 사용해 datetime type으로 인덱스를 바꿔주는 게 좋을 것이다.
그러면 다음과 같이 10년치 애플 가격이 받아와진다.
처음에 여기서 한번 실망을 했다. 왜 수정 종가밖에 없는거지? 하고. 그런데 알고보니 오른쪽 두 열이 힌트였다. dividend amount가 배당이고 split coefficient가 액면분할계수다. 참고로 기업의 주식배당(stock dividends)는 원 글을 참고하면, 주식 지출이 (회사 장부로부터 주주에게로) 이미 존재하는 주식의 소유권만 변경하기 때문에 과거의 가격 수정이 필요하지 않다고 한다. 즉 기업의 시가 총액이 변하는 것이 아니므로 가격을 수정할 필요가 없는 것이다.
배당 및 주식 액면분할 효과란 즉 액면분할하면 액면분할계수대로 나누고, 배당을 주면 주당 현금 배당금만큼 주가에서 빼면 된다. 상식적이다. 위는 그것을 공식화한 것인데, 주의할 점은 'S(Split ratio)'다. 1대 7 액면분할을 하면 S=1/7이 된다. 그래서 공식에서는 바로 위에서 말한 것과 다르게 '곱하기'로 표시되어 있는 것이다.
이제 이것을 코드화하는 일만 남았다.
def calculate_adjusted_prices(df: DataFrame, column: str, split_coefficient: str, dividend: str):
"""
Refer to https://joshschertz.com/2016/08/27/Vectorizing-Adjusted-Close-with-Python/
Vectorized approach for calculating the adjusted prices for the
specified column in the provided DataFrame. This creates a new column
called 'adj_<column name>' with the adjusted prices. This function requires
that the DataFrame have columns with dividend and split_ratio values.
:param df: DataFrame with raw prices along with dividend and split_ratio
values
:param column: String of which price column should have adjusted prices
created for it
:param split_coefficient: String of split coefficient column.
e.g. split_coefficient=7 means that 1 stock is split into 7 stocks.
:param dividend: String of dividend column
:return: DataFrame with the addition of the adjusted price column
"""
adj_column = 'adj_' + column
# Remove redundant column
if adj_column in df.columns:
del df[adj_column]
# Reverse the DataFrame order, sorting by date in descending order
df.sort_index(ascending=False, inplace=True)
# Extract values
price_col = df[column].values
split_col = df[split_coefficient].values
dividend_col = df[dividend].values
adj_price_col = np.zeros(len(df.index))
adj_price_col[0] = price_col[0]
# Calculate adjusted prices
for i in range(1, len(price_col)):
adj_price_col[i] = \
round(
number=(adj_price_col[i - 1] + adj_price_col[i - 1] * (((price_col[i] / split_col[i - 1]) - price_col[i - 1] - dividend_col[i - 1]) / price_col[i - 1])),
ndigits=4
)
df[adj_column] = adj_price_col
# Change the DataFrame order back to dates ascending
df.sort_index(ascending=True, inplace=True)
return df
위 공식을 벡터화하여 계산하는 방법이다. 사실 원작자는 처음엔 loop를 사용해서 현재부터 과거까지 하나하나 계산했는데, 긴 시계열에서 아주 오랜 시간이 걸렸다고 한다. 그래서 이 연산을 벡터화(링크)한 코드가 위의 코드다. 이론적인 부분까지 깊게 들어가긴 어렵다. 그의 헌신을 감사히 여기고 그의 코드를 사용하자. 위 코드는 원본에서 약간의 수정을 거쳤다. av의 split coefficient column에는 1대 n 액면분할시에 1/n이 아닌 n이 그대로 들어가있었다. 해당 부분을 수정했다. (나의 헌신도 한방울 포함되어있다.)
코드 자체는 이해하기 어렵지 않다. dataframe의 필요한 각 column을 numpy에 넣어서 열벡터로 각각 만들고, 위에 언급한 공식을 그대로 적용해주는 것이다. 인접한 가격이 수정되는것을 반영해주어야 하므로 시계열 길이만큼 반복해준다.
메서드는 만들어져있으니 적용해본다.
for price in ['open', 'high', 'low', 'close']:
ret_df = calculate_adjusted_prices(df_AAPL_av, price, 'split coefficient', 'dividend amount')
이런 dataframe이 만들어진다.
av에서 기본으로 제공하는 adjusted close column과 직접 계산한 adj_close column의 값을 비교해보면, 아주 먼 과거에서도 별로 큰 차이가 없다. 내가 FinanceDataReader를 아쉬워했던것이 이 부분이다. FinanceDataReader에서는 먼 과거 시점의 가격 정보가 꽤 틀어져있었기 때문이다. 원본 OHLC가격 정보, 배당금이나 액면분할계수는 그냥 과거의 사실 그 자체임에도 불구하고 계산한 값과 다르다는 것을 받아들이기 어려웠다.
먼저 라이브 트레이딩을 위해서는 실시간으로 주가를 수집해야 할 수 있고, 백테스팅을 위해서라면 과거 데이터를 한꺼번에 불러올 수 있어야 한다. 나는 우선 안정적으로 과거 데이터를 수집할 방법이 필요하다.
몇몇 방법들을 시험해 봤는데. 아직도 확실한 방법을 못 찾겠다. 몇 가지 방법은 있다. 1. 증권사 API와 트랜잭션하기 2. FinanceDataReader 패키지(git 링크) 3. pandas_datareader 패키지(doc 링크) 4. alpha-vantage API(doc 링크)
우선 1번은 다른 방법에 비해서 가장 디테일한 정보를 받을 수 있다. 하지만 다루기가 다른 방법들에 비해서 좀 까다롭다. 증권사마다 계좌도 파야 하고, 무엇보다 해외 시장 데이터는 가져올 수 없다. 게다가 파이썬에서 직접적으로 COM 트랜잭션 할 방법은 없기 때문에 PyQt같은 패키지를 통해 간접적으로 접근해야 하는 등... 귀찮다. 결국엔 이용해야 하는 종착지라 현재는 보류중. 난 좀 더 간단한 방법이 필요하다.
2번은 정말 간편하다. 간단한 import문 한방에 한국, 미국 전 세계 다양한 시장의 증권 수정가격을 dataframe으로 불러올 수 있다. 게다가 미국/한국의 채권 수익률, 환율이나 암호화폐 시계열도 지원한다! 정말 좋은 패키지다. 문제는 데이터의 양이 조금만 많아지면 데이터를 불러오다 말고 끝난다. 정말 사용하기 간편하고 좋은데 S&P500같은 긴 시계열에서는 불완전하다고 느꼈다. 결국 온전히 불러오기 위해선 추가적인 품이 들어간다. https://financedata.github.io/posts/finance-data-reader-users-guide.html
위 문서를 보니 한번에 가져오는 데이터의 건 수가 5000개라고 되어있다. 여러번에 나누어 불러오는 메서드가 필요할 것 같다.
3번은 가장 일반적인 방법으로 보통 Yahoo Finanace API랑 연동해서 불러오는 경우가 많다. 조금만 손쓰면 국내 주식시장 가격도 불러오는 게 가능하다. 그런데 야후파이낸스 API 지원이 끊겼다는 얘기가 있다(링크). 그래서 구글링을 좀만 해도 야후파이낸스 API의 대체수단을 찾는 사람이 좀 있다. 사실 얘 아직 열심히 써보진 못했다. 또한 해당 패키지를 통해 quandl API, Fama/French API등등을 사용할 수도 있다.
4번은 3의 대안으로 찾은 것. 미국 시장/외환 시장 정보를 불러올 수 있다. 이 패키지(링크)를 사용하면 alpha-vantage의 데이터를 좀 더 손쉽게 가져올 수 있다. 완전 자유롭게 데이터를 읽어오려면 유료이나 약간의 시간적 제약과 함께 무료로 사용할 수도 있다. 사용성도 간편한 편이고 가장 큰 장점은 intra-day(장중)가격 데이터를 1분, 5분, 15분, 30분, 60분 간격으로 읽어올 수도 있다는 점이다! 물론 모든 정보를 다 저장하려면 공간 제약이 있을 것이다. 그리고 기술적 지표를 함께 제공하는 것도 큰 장점이다. 단점은 사이트에 가입해서 받을 수 있는 API키가 강제된다는 것이며, 불러올 수 있는 가장 먼 가격 데이터가 20년 전이었던 것 같다.