텐서플로우로 구글 클라우드 플랫폼 상에서 금융 데이터를 기계 학습하기

이 솔루션은 구글 클라우드 플랫폼(이하 GCP) 상에서 금융 시계열 데이터로 기계 학습을 한 실제 사례를 소개합니다.

시계열 데이터는 금융 분석의 핵심 분야 입니다. 오늘날, 당신은 다양한 출처로부터 더 빈번하게 배달되어 오는 더 많은 데이터를 가지게 되었습니다. 이런 출처에는 새로운 교류, 소셜 미디어 아웃렛, 그리고 뉴스를 포함합니다. 또한 10년 전 초당 수십 개의 메세지로부터 오늘날에 이르러 수십만 개로 전달 빈도가 증가했습니다. 이를 극복하기 위한 결과로써 자연적으로 다양한 분석 기법들이 등장했습니다. 현대 분석 기법들의 대부분은 통계를 기반으로 하고 있다는 면에서 새롭지는 않지만 적용 가능성이 가용한 컴퓨팅 파워의 증가 추세를 따르고 있다는 점에 주목할 필요가 있습니다. 이렇게 가용한 컴퓨팅 파워의 성장 속도가 시계열 데이터의 증가보다 더 빨라서 규모의 문제로 인해 이전에는 실용적이지 않아 불가능했던 시계열 데이터를 분석할 수 있게 되었습니다.

특히, 딥러닝과 같은 기계학습 기법은 시계열 분석에 희망적입니다. 시계열 데이터가 점점 더 조밀해지고 많은 부분에서 겹쳐지게 될수록 기계학습은 소음이 엄청나게 보일지라도 소음에서 신호를 분리해 내는 방법을 제공할 것입니다. 딥러닝은 금융의 시계열 데이터가 보여주는 무작위적 특성에서도 최적 해(best fit)를 찾을 수 있기에 엄청난 가능성을 가지고 있습니다.

이 솔루션에서는 당신은 다음과 같은 일을 수행하게 됩니다:

  • 여러 금융 시장에서 데이터 획득
  • 데이터를 쓸수 있는 형태로 분리하고 전제(premise)를 탐색하고 검증하기 위해 탐색적 데이터 분석 (EDA)를 수행
  • 텐서플로우로 금융 시장에서 일어날 일들을 예측하기 위한 여러 모델을 만들고, 학습(train)해서 평가

당신은 이 모든 일들을 클라우드 데이터랩 노트북에서 수행하게 될 것입니다.

경고: 이 솔루션은 GCP와 텐서플로우가 빠르고 상호작용적이며 반복적인 데이터 분석과 기계학습에 적절한 지 보여주기 위해 만들어 졌습니다. 따라서 이 솔루션은 금융 시장이나 매매 전략에 어떠한 조언도 제공하지 않습니다. 이 설명서에 나타난 시나리오는 설명서에 나타난 시나리오는 예제에 불과합니다. 절대 투자를 결정하는 데 있어 이 코드를 사용하지 마십시오!

선행 요건

노트: 클라우드 데이터랩 상의 이 노트북을 읽거나 가치를 끌어내기 위해서 복제 본을 수행하거나 다운로드할 필요는 없습니다. 그러나 이것을 변경시키거나 적극적으로 장려하는 자신의 목적에 맞게 시험해 보기 원한다면 다음 선행 요건을 완료해야 합니다.

전제(The premise)

여기의 전제는 명료합니다: 금융시장이 세계화되어서 만일 태양이 뜨는 순서를 따라 아시아로부터 유럽 그리고 미국 등으로 따라가면 빠른 시간대에서 얻은 정보로 늦은 시간대의 국가에서 이득을 볼 수 있을 것입니다.

다음 표는 전 세계의 많은 주식 시장과 미국 동부 시간대(EST) 기준의 폐장 시장, 그리고 뉴욕의 S&P 500의 마감 시간 간의 차이를 보여줍니다. 이 표에서는 EST를 기준 시간 대로 사용하는데 예를 들면, 호주 시장은 미국 시장이 폐장하기 15시간 전에 닫습니다. 만일 호주의 All Ords(AOI)의 마감이 주어진 날의 S&P 500 마감의 유용한 예측 변수(predictor)가 된다면, 우리는 이 정보를 우리의 거래 지침을 주는 정보로 활용할 수 있습니다. 호주의 AOI를 일례로 계속해보죠. 만일, AOI 지수가 마감되고 이후 S&P 500 또한 머지않아 마감될 것이기에 이것으로 우리는 S&P 500에 포함된 주식 (혹은 보다 그럴듯하게 S&P 500를 따르는 ETF)를 살 지를 고려해야 한다고 합시다. 사실 현실에서는 상황은 훨씬 더 복잡한데 그 이유는 수수료와 관련 세금이 추가되기 때문입니다. 그러나, 처음 근사 치로써, 우리는 이 마감 지수가 이득을 나타내고 그 반대로도 그렇다고 가정할 것입니다.

인덱스 국가 폐장시간 (EST) S&P 폐장 시간과의 차이
All Ords 호주 0100 15
Nikkei 225 일본 0200 14
Hang Seng 홍콩 0400 12
DAX 독일 1130 4.5
FTSE 100 영국 1130 4.5
NYSE Composite 미국 1600 0
Dow Jones Industrial Average 미국 1600 0
S&P 500 미국 1600 0

셋업

먼저, 필요한 라이브러리들을 불러옵시다.

불러들인 라이브러리들은 다음과 같습니다:

  • pd로 지칭(alias)된 pandas는 BSD 라이센스의 라이브러리로 고 성능의 손 쉬운 데이터 구조와 데이터 분석 도구를 제공합니다. 이 라이브러리의 auto correlation plot과 scatter matrix를 에서 도식화를 위해 활용하고 있습니다.
  • numpy 역시 BSD 라이센스의 라이브러리로 파이썬에서 과학적 컴퓨팅을 위한 기본 패키지이며 N차원 배열 연산을 손쉽게 수행할 수 있는 기능들을 제공합니다.
  • matplotlib은 2차원 차트 라이브러리를 손쉽게 그릴 수 있도록 해 줍니다.
  • gcp는 Google Cloud Platform의 기능을 데이터랩에서 손쉽게 활용할 수 있도록 제공된 라이브러리 입니다. 이 예제에서는 big query 서비스를 이용합니다. 자세한 내용은 데이터랩의 README를 참고하세요.
  • tensorflow는 딥러닝을 위해 제공되는 라이브러리입니다.
In [3]:
import StringIO

import pandas as pd
from pandas.tools.plotting import autocorrelation_plot
from pandas.tools.plotting import scatter_matrix

import numpy as np

import matplotlib.pyplot as plt

import gcp
import gcp.bigquery as bq

import tensorflow as tf

데이터 얻어오기

이 데이터에는 2010년 1월 1일부터 2015년 10월 1일까지 지난 5년 여 간의 기록을 담고 있습니다. 데이터는 S&P 500 (S&P), NYSE, Dow Jones Industrial Average (DJIA), Nikkei 225 (Nikkei), Hang Seng, FTSE 100 (FTSE), DAX, and All Ordinaries (AORD) 인덱스에서 왔습니다.

이 데이터는 공개되어 있고 편의를 위해 빅쿼리에 저장되어 있으며 클라우드 데이터랩의 빌트인 컨넥터 기능으로 Pandas 데이터프레임으로 가져올 수 있게 됩니다.

먼저, %%로 시작하는 명령은 노트북 외부의 bash command를 실행할 수 있도록 제공되는 기능입니다. 코드에서는 $market_data_table 테이블에서 SQL 문을 통해 Date와 Close 컬럼을 가져와서 market_data_query 모듈에 저장한 다음 빅쿼리 API로 각각 데이터프레임으로 저장하게 됩니다. 자세한 예제는 빅쿼리 튜토리얼 을 참고하시기 바랍니다.

In [4]:
%%sql --module market_data_query
SELECT Date, Close FROM $market_data_table
In [5]:
snp = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.snp')).to_dataframe().set_index('Date')
nyse = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.nyse')).to_dataframe().set_index('Date')
djia = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.djia')).to_dataframe().set_index('Date')
nikkei = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.nikkei')).to_dataframe().set_index('Date')
hangseng = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.hangseng')).to_dataframe().set_index('Date')
ftse = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.ftse')).to_dataframe().set_index('Date')
dax = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.dax')).to_dataframe().set_index('Date')
aord = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.aord')).to_dataframe().set_index('Date')

데이터 정리하기(Munge the data)

첫번째 시도로써 데이터를 정리하는 건 명확합니다. 종가(closing prices)가 관심사이기 때문에 편의를 위해 각각의 인덱스에서 종가를 추출해서 closing_data 라는 Pandas 데이터프레임에 저장합니다. 모든 인덱스 값들이 주로 거래소 휴장등의 이유로 값들을 가지지 않는 경우가 있기 때문에 이 차이를 미리 메꿀(forward-fill) 겁니다. 즉, 어떤 날(day N)에 값이 없다면 전날(N-1)이나 그 전전날(N-2)의 값을 채워서 최신 값이 포함되도록 합니다.

In [4]:
closing_data = pd.DataFrame()

closing_data['snp_close'] = snp['Close']
closing_data['nyse_close'] = nyse['Close']
closing_data['djia_close'] = djia['Close']
closing_data['nikkei_close'] = nikkei['Close']
closing_data['hangseng_close'] = hangseng['Close']
closing_data['ftse_close'] = ftse['Close']
closing_data['dax_close'] = dax['Close']
closing_data['aord_close'] = aord['Close']

# Pandas includes a very convenient function for filling gaps in the data.
closing_data = closing_data.fillna(method='ffill')

지금 이 순간, 이 노트북의 코드 20 여 줄로 여덟 개의 금융 인덱스들에서 각각 5년 간의 시 계열 데이터를 확보했고 관련된 데이터를 얽어서 하나의 데이터 구조로 만든 후, 같은 수의 엔트리를 갖도록 일치시켰습니다. 덧붙여, 이 모든 것을 수행하는 데 약 10초 밖에 걸리지 않습니다. 이는 감동적인 부분인데, 첫번째 iPython 노트북을 통해 파이썬의 우수함을 활용하고 두번째 GCP 서비스에 연결할 수 있는 호스트를 제공함으로써 어떻게 구글 클라우드 데이터랩이 생산성을 확대하는가에 보여줍니다. 당신이 지금 빅쿼리만 써 봤고 구글 클라우드 스토리지를 경험하지 않았지만, 점차 이런 컨넥터의 수가 증가하는 것이 눈에 보이길 기대하게 될 것입니다.

탐색적 데이터 분석(EDA)

탐색적 데이터 분석(EDA)는 기계학습으로 작업하는 것과 다른 종류의 어떤 분석들의 기초가 됩니다. EDA는 데이터를 점점 더 이해하게 되고, 데이터에 손을 더럽히게 되며, 데이터를 보고 느끼는 것을 의미합니다. 그 최종 결과는 데이터를 매우 잘 이해할 수 있게되어 모델을 세울 때 그 모델을 모호한 편견이나 가정이 아닌 현실적이고 실제적이자 구체적인 이해를 기반으로 만들 수 있게 됩니다.
물론 당신은 여전히 어떤 가정들을 만들 수 있겠지만 EDA는 당신의 가정과 이 가정들을 왜 만들게 되는 지를 이해하게 된다는 것을 의미하게 됩니다.

먼저, 다음 데이터를 살펴 봅시다.

In [5]:
closing_data.describe()
Out[5]:
snp_close nyse_close djia_close nikkei_close hangseng_close ftse_close dax_close aord_close
count 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000
mean 1549.733275 8920.468489 14017.464990 12529.915089 22245.750485 6100.506356 7965.888030 4913.770143
std 338.278280 1420.830375 2522.948044 3646.022665 2026.412936 553.389736 1759.572713 485.052575
min 1022.580017 6434.810059 9686.480469 8160.009766 16250.269531 4805.799805 5072.330078 3927.600098
25% 1271.239990 7668.234863 11987.635254 9465.930176 20841.259765 5677.899902 6457.090088 4500.250000
50% 1433.189941 8445.769531 13323.360352 10774.150391 22437.439453 6008.899902 7435.209961 4901.100098
75% 1875.510010 10370.324707 16413.575196 15163.069824 23425.334961 6622.650147 9409.709961 5346.150147
max 2130.820068 11239.660156 18312.390625 20868.029297 28442.750000 7104.000000 12374.730469 5954.799805

다음과 같이 수의 비교(Orders of magnitude)로 다르게 한 비례를 기반으로 운영되는 다양한 인덱스를 볼 수 있습니다. 이 데이터를 비율로 맞추는 것이 좋은데 예를 들어, 하나의 엄청나게 큰 인덱스에 의해 여러 인덱스를 포함하는 동작에 과도하게 영향을 미치지 않게 되기 때문입니다.

이 데이터를 그려봅시다.

코드는 paddas의 concatenate 함수를 통해, 각 인덱스 별 종가 데이터를 연결하고 concaternated objects에 plot 함수로 데이터를 line chart로 도식화합니다. 여기서 인자 axis는 연결할 축 갯수를 지정하고 figsize는 인치 당 튜플(넓이, 높이)를 지정합니다. 반환된 값을 저장하는 _ 변수는 고의적으로 버리려는(throwaway) 목적으로 관습적 명명법에 따라 정의했습니다. 주석에 따르면 처리 결과를 보여주지 않을 목적으로 활용되었다고 합니다.

In [35]:
# N.B. A super-useful trick-ette is to assign the return value of plot to _ 
# so that you don't get text printed before the plot itself.

_ = pd.concat([closing_data['snp_close'],
  closing_data['nyse_close'],
  closing_data['djia_close'],
  closing_data['nikkei_close'],
  closing_data['hangseng_close'],
  closing_data['ftse_close'],
  closing_data['dax_close'],
  closing_data['aord_close']], axis=1).plot(figsize=(20, 15))

기대했던 것처럼, 인덱스 별로 구조가 동일하게 보이지 않습니다. 각각의 인덱스의 값들을 그 인덱스의 최대 값으로 나누고 다시 도식을 그려 봅니다. 모든 인덱스의 최대 값은 1이 될 것입니다. 코드는 이 값들을 *_close_scaled 를 postfix로 붙여 pandas 데이터프레임에 저장합니다.

In [36]:
closing_data['snp_close_scaled'] = closing_data['snp_close'] / max(closing_data['snp_close'])
closing_data['nyse_close_scaled'] = closing_data['nyse_close'] / max(closing_data['nyse_close'])
closing_data['djia_close_scaled'] = closing_data['djia_close'] / max(closing_data['djia_close'])
closing_data['nikkei_close_scaled'] = closing_data['nikkei_close'] / max(closing_data['nikkei_close'])
closing_data['hangseng_close_scaled'] = closing_data['hangseng_close'] / max(closing_data['hangseng_close'])
closing_data['ftse_close_scaled'] = closing_data['ftse_close'] / max(closing_data['ftse_close'])
closing_data['dax_close_scaled'] = closing_data['dax_close'] / max(closing_data['dax_close'])
closing_data['aord_close_scaled'] = closing_data['aord_close'] / max(closing_data['aord_close'])
In [37]:
_ = pd.concat([closing_data['snp_close_scaled'],
  closing_data['nyse_close_scaled'],
  closing_data['djia_close_scaled'],
  closing_data['nikkei_close_scaled'],
  closing_data['hangseng_close_scaled'],
  closing_data['ftse_close_scaled'],
  closing_data['dax_close_scaled'],
  closing_data['aord_close_scaled']], axis=1).plot(figsize=(20, 15))