https://pandas-datareader.readthedocs.io/en/latest/remote_data.html#naver-finance-data

 

Remote Data Access — pandas-datareader 0.9.0rc1+2.g427f658 documentation

Functions from pandas_datareader.data and pandas_datareader.wb extract data from various Internet sources into a pandas DataFrame. Currently the following sources are supported: It should be noted, that various sources support different kinds of data, so n

pandas-datareader.readthedocs.io

분명 얼마전에 봤을 땐 못 본거같은데....  pandas-datareader정도 되는 글로벌(?) 패키지가 네이버 금융, Naver Finance에 원격 접속을 지원합니다. 예시도 친절하게 우리나라 대표주식인 삼성전자를 올려놨네요ㅋㅋㅋ

뭐 아예 이렇게 네이버 페이지를 데이터마이닝 하는 것도 보긴봤는데... https://excelsior-cjh.tistory.com/109

아무튼 pandas-datareader에서 보여준 예시대로 해보니까 안됩니다. data source가 구현이 안돼있대요.

참고로 pandas-datareader 0.9.0v으로 업데이트 해도 안됐습니다. 그러던 중 한 페이지를 더 발견했는데요

https://pandas-datareader.readthedocs.io/en/latest/readers/naver.html?highlight=naver

 

Naver Finance — pandas-datareader 0.9.0rc1+2.g427f658 documentation

symbols – A single symbol; multiple symbols are not currently supported.

pandas-datareader.readthedocs.io

아예 문서가 존재했었네요.

# import pandas_datareader.data as web
import pandas_datareader.naver as web_naver

뭐 이런식으로 import를 해야하나봅니다. 보통은 주석처럼 'data'라는 하위 클래스에서 다 읽어올 수 있었는데.

df_price_SEC = web_naver.NaverDailyReader(symbols='005930', start='19800101')
df_adj_price_SEC = web_naver.NaverDailyReader(symbols='005930', start='19800101', adjust_price=True)

이런식으로.. 읽어올 수 있고요. 그러나 이게 끝이 아닙니다.

#%%
df_price_SEC = df_price_SEC.read()
df_price_SEC

#%%
df_adj_price_SEC = df_adj_price_SEC.read()
df_adj_price_SEC

pandas dataframe으로 만들려면 read()라는 메서드까지 써야 제대로 출력됩니다.

그럼 이렇게 OHLCV로 읽어와지는데, 1990년 1월초부터의 데이터밖에 불러올 수 없습니다. 그리고 과거의 저 가격이 이상하게 높은거 같아서 봤더니 역시 뭔가 이상하네요. 수정가격이 아닌것 같습니다.

adjusted_price=True로 패러미터를 넘겨도 결과는 같아요.

 

설레서 알아봤는데, 그냥 yahoo finance나 FinanceDataReader쓰는게 나을거같아요.

주가 수익률이 정규분포를 따른다고 가정한 금융 모델들이 많이 있지만, 사실 정말 그런건지 의심이 되었습니다.

그래서 파이썬의 scipy패키지를 이용해서 실험적으로 확인해봤어요.

import numpy as np
import pandas as pd
import FinanceDataReader as fdr

from scipy.stats import norm, laplace, t

import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (12, 9)
plt.rcParams['figure.dpi'] = 300
plt.rcParams['savefig.dpi'] = 300
plt.rcParams['lines.linewidth'] = 1
plt.rcParams['lines.color'] = 'r'
plt.rcParams['axes.grid'] = True

먼저 패키지 import하고 matplotlib.pyplot의 전역 설정을 해줍니다

fig1 = plt.figure(1)

# SPY daily return
ax1 = plt.subplot(511)
ax1.set_title('Historical S&P500(SPY) daily log returns')

# Normal distribution
ax2 = plt.subplot(512, sharex=ax1)
ax2.set_title('Normal dist.')

# Laplace distribution
ax3 = plt.subplot(513, sharex=ax1)
ax3.set_title('Laplace dist.')

# Student's T distribution
ax4 = plt.subplot(514, sharex=ax1)
ax4.set_title('Student T\'s dist.')

# Total
ax5 = plt.subplot(515, sharex=ax1)
ax5.set_title('Comparison')

subplot 설정해주고

df_SPY = fdr.DataReader('SPY')

bday = df_SPY.index
count_bday = len(bday)
bin_div10 = int(count_bday / 10)
print(f'counted business days: {count_bday}')

counted business days: 6934

SPY 가격정보와 장 열린 날 수를 계산해봅시다.

SPY_close = pd.Series.to_numpy(df_SPY['Close'])
log_ret_SPY_close = np.diff(np.log(SPY_close))
ax1.hist(log_ret_SPY_close, density=True, histtype='stepfilled', alpha=0.5, bins=bin_div10)

로그 수익률을 계산합니다. 로그수익률을 사용하면 일반수익률에 비해 아래와 같은 장점이 있습니다.

  • '0'을 중심으로 대칭적(확률 모형 계산에 용이)
  • 여러 기간의 로그 수익률 간 단순 합산(+)으로 전체 기간의 로그 수익률 계산 가능

로그 수익률에 대해서 더 자세히 알고싶으면 여기:
http://tedware.kr/posts/212
https://en.wikipedia.org/wiki/Rate_of_return#Logarithmic_or_continuously_compounded_return

그리고 histogram으로 누적 분포를 plot해줍니다.

 

S&P500(SPY ETF)의 일간 주가 로그 수익률과 여러 확률분포의 비교

결과는 미리 보여드릴게요.

맨 위는 실제 과거의 S&P500 주가 일간 수익률

아래 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에 가까운 움직임은 약간 모자란게 조금 아쉽습니다.

라플라스분포는 특성상 중심이 뾰족하고 fat-tail이 잘 설명되는 특징이 있습니다.

주가 확률 보행 모델을 만들땐 라플라스 분포를 이용하면 좋겠군요.

오늘 참고한 글: https://tradeoptionswithme.com/probability-dsitribution-of-stock-market-returns/

 

The True Probability Distribution of Stock Market Returns

In This Article, You Will Learn The True Probability Distribution Of Stocks And The Significant Risks Associated With Being Unaware Of It.

tradeoptionswithme.com

Cauchy 분포

참고로 코시분포를 사용하면 진짜 black swan이 모델링될 수도 있습니다. 근데 좀 다루기가 어려운게 평균이나 분산이 존재하지 않는 특이한 확률분포에요. 이걸 써먹으러면 parameter fitting 잘 되어야 할 것 같네요.

바로 이전 글에서 어떤 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 등에서 이미 수정된 가격을 가져오는 것 같았다. 그런데 아무리 봐도 아쉬운 점이 있었으니, 과거 데이터로 갈수록 실제 수정 가격이랑 약간 오차가 있었다.*

*오차가 있다고 스스로 결론지었지만, 잘못된 정보를 드렸을 가능성이 있으므로 여러분도 직접 계산을 해보길 바랍니다.

주말 간 약간의 구글링과 시행착오 끝에 수정 가격 구하는 코드를 짤 수 있었다.

참고한 글: 링크

 

Vectorizing Adjusted Close with Python

Adjusted prices are essential when working with historical stock prices. Any time there is a corporate split or dividend, all stock prices prior to that event need to be adjusted to reflect the change.

joshschertz.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...부분은 앞으로 그릴 플롯의 형태를 미리 전역 설정해주는것이다. 자세한건 나중에 따로 공부해야지.

date_today = dt.date.today()
date_10yrs_ago = date_today - relativedelta(years=10)

10년 전 오늘의 날짜를 정확하게 가져오려면 이러한 방식을 사용해야 한다.
이해가 안 가는 것이 있다면 datetime 패키지의 timedelta 클래스를 사용해서는 1년 이상의 날짜 연산이 안된다.
timeutil의 relativedelta 클래스를 사용해줌으로써 해결.

av_apikey = 'YOUR API KEY'
df_AAPL_av = web.DataReader('AAPL', 'av-daily-adjusted', start=date_10yrs_ago, api_key=av_apikey)

중요한 부분이다. 애플의 10년 가격 정보와 수정 정보를 받아오는것인데, 그 전에 먼저 alpha-vantage(이하 av)에 무료 회원가입을 하고서 API 키를 받아야한다(링크).
가입하면서 알려주는 API 키는 한번 창을 끄면 다시는 알려주지 않으므로 반드시 따로 저장해놓도록 한다. 이렇게 받은 API키는 av_apikey 변수에 string으로 할당해놓자.
pandas_datareader의 DataReader 메서드를 통해 av API를 사용할 수 있다.
첫 번째 패러미터로 티커를, 두번째는 'av-daily-adjusted'를 입력해줌으로써 av의 일간 수정 가격을 받아오겠다고 명시하자. 세 번째에는 아까 받은 API key를 꼭 제공해야 한다.

df_AAPL_av.index = pd.to_datetime(df_AAPL_av.index)
df_AAPL_av.index.name = 'Date'

그리고 약간의 가공을 해주자. 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가격 정보, 배당금이나 액면분할계수는 그냥 과거의 사실 그 자체임에도 불구하고 계산한 값과 다르다는 것을 받아들이기 어려웠다.

df_APPL_adj_ohlc = ret_df.loc[:, ['adj_open', 'adj_high', 'adj_low', 'adj_close', 'volume']]
df_APPL_adj_ohlc.columns = ['Open', 'High', 'Low', 'Close', 'Volume']
mpf.plot(df_APPL_adj_ohlc, type='candle')

이제 수정 가격만을 가져와서 정리해준다. 
눈에는 잘 안띄지만, 수정 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

 

FinanceDataReader 사용자 안내서

FinanceDataReader 사용자 안내서

financedata.github.io

위 문서를 보니 한번에 가져오는 데이터의 건 수가 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년 전이었던 것 같다.

5번은 더이상 알아보기 힘들어서 보류함.

1번은 필수로 거쳐야 할 것이고, 2, 3, 4중에서 좀더 조사를 해봐야지.

+ Recent posts