: 주식 차트 데이터에 대한 보조 지표는 이미 많은 사람들이 사용하고 있으며, 기존의 방식에 더하여 자신만의 지표를 개발하는 사람도 많다. 이번 차트 데이터에 대한 전처리는 지난 번과 마찬가지로 코인 시장의 한 종목에 대한 차트 데이터를 이용하였으며, 1시간 봉을 기준으로 설정했다.
: 위의 데이터는 KRW-XRP의 1시간 봉 차트이며, 날짜는 9/2~10/12, 약 한 달 조금 넘는 기간을 대상으로 했다. 딱 차트만 봐도 위의 차트는 수익을 내기 굉장히 어려운 차트인 것 처럼 보인다. 매수를 해야하는 시점이 얼마 없기 때문이다. 그러나, 이러한 차트에 대해서도 수익을 내거나 손실을 최소화시키는 모델을 만드는 것이 목적이기 때문에, 학습을 하기에는 좋은 차트임에는 틀림없다.
먼저, 본인은 기간 및 종목, 기준 봉을 입력하면 그에 해당하는 Data를 가져오는 Data Loader을 미리 만들어 놨다.
from lib.data_loader import DataLoader
import matplotlib.pyplot as plt
import pandas as pd
dataLoader = DataLoader()
q, p = dataLoader._make_query('20200902000000', '20201012000000', 'minute60', 'KRW-XRP')
training_data, chart_data = dataLoader._run(q, p)
chart_data.describe()
위의 모듈을 적용하면, 미리 세팅해 놓았던 Training Data와 Chart Data를 반환하게끔 만들었으며, Moving Average(MA)를 구하면서 발생하게되는 Null 값을 처리하기 때문에 실제 기간보다 조금 더 줄어들게 된다.
chart_data.describe()는 pandas dataframe의 한 method로 Data에 대한 전반적인 정보를 살펴볼 수 있다.
여기서 open, high, low, close는 각각 시가, 고가, 저가, 종가를 의미하고 volume은 거래량을 의미한다. 또한, 좌측의 index를 살펴보면 count는 Null 값(결측치)이 아닌 실제 값들에 대한 개수를 의미하며, 이 값이 현재 데이터의 총 row 개수인 960보다 작으면 결측치가 있다고 판단할 수 있다. 그리고 각 column에 대한 기본 통계 수치도 확인할 수 있다.
일단 애초에 본인이 만들어 놓은 Local DB에 데이터를 적재할 때, 결측치가 발생하지 않게끔 실시간 적재를 하고 있지만, 그래도 혹시나 결측치가 있을 경우, 이에 대한 처리를 하는 과정을 추가했다.
1. Null 값 처리
: Data를 다루는 사람들이라면, 한 번 쯤은 시행했을 전처리이다. 결측치의 처리는 Data를 분석할 때, 결과를 좌지우지할 수 있을 정도로 중요한 부분이면서, 가장 기본적인 절차다. 이번에도 예외는 없다.
chart_data.index = pd.to_datetime(chart_data.date)
chart_data = chart_data.iloc[:, 1:]
a = chart_data.isnull().sum(0)
nullCol = [x for x in a.index if a[x] != 0]
if len(nullCol) > 0:
for c in nullCol:
print('{} has Null'.format(c))
chart_data[c] = chart_data[c].interpolate(method='time')
코드에 대한 설명은 불친절할 수도 있다. 왜냐하면, 코드를 하나씩 설명해가며 글을 적기에는 너무나 많은 시간이 걸리기 때문에 코드와 관련해서는 나중에 기회가 있을 때, 포스팅할 예정이다.
위의 코드는 Null 값(결측치)이 있을 경우, Interpolation (보간법)을 시행한다. 결측치에 대한 처리 방법은 다양하게 있지만, .fillna(method='bfill') or .fillna(method='ffill') 같은 경우에는 결측치의 구간이 길어질 때 오히려 noise로 작용할 우려가 있어, 사용하지 않았다. 대신에, chart_data는 시간을 인덱스로 사용하기 때문에, 시간에 Weighted한 interpolation을 적용하기로 했다. (의미를 모를 경우, 구글에서 interpolate with time method 검색)
2. 종가 (Close Price) Normalization
: 종가라고 적어놨지만, 사실상 이번 Data 분석에서 사용할 가격에 대한 Data를 정규화시키는 가장 가장 가장 중요한 부분이다. 본인도 어떻게 하면 강화학습의 Policy Network (정책신경망)을 잘 학습시킬 수 있을지 많은 고민을 했다.
먼저, 종가의 Normalization을 언급하기 전에, Normalization, Standardization, Generalization 에 대한 구분을 모른다면, 한 번 쯤 검색해서 읽고 오길 바란다.
결론적으로, 종가에 대해서는 min-max normalization을 적용하기로 결정했다. Standardization과 Normalization 사이에서 어떤 방법을 적용하는게 가장 Robust한 모델을 만들 수 있을지에 대해 초점을 맞춰 고민했다. 여기에 모든 과정을 적을 수는 없지만, 간단한 이유는 다음과 같다.
Starndardization은 특정 기간에 대한 가격의 평균과 표준편차를 구하여, 이를 평균이 0, 표준편차가 1인 분포로 표준화를 시키는 과정이다. 물론, 많은 논문에서도 Data를 그대로 사용하는 것보다 표준화를 시켜서 학습하는 것이, 성능이 더 좋다는 결과가 많기 때문에 이미 많은 사람들이 알고 있을 것이다.
Outlier에 대해서도 min-max Normalization과 비교했을 때, 안정적인 전처리 방식이기도 하다. 그럼에도, Standardization을 선택하지 않은 이유는 "평균 변화에 대한 민감도"가 "최고 - 최저가의 변화에 대한 민감도" 보다 더욱 크기 때문이다.
예를 들어, 최근 3개월의 가격 평균을 기준으로 표준화시키고, 이를 학습 데이터로 학습을 시켜 모델을 만들었다고 하자. 만약 한 달이 지난 시점 가격에 대한 상승 변동이 있다고 하면, 전체적인 가격에 대한 평균이 더욱 올랐을 것이다. 즉, 현재 시점의 가격을 한 달 전에 계산한 평균과 분산을 가지고 표준화를 시켰을 때, 그 영향도가 실제로 같지 않을 것이라는 의미이다.
조금 더 쉽게 설명하면, 280원이라는 가격이 한 달 전에는 30원 정도의 가치였다면, 현재 280원의 가치는 20원 정도로 줄어들었다는 얘기이다. 그럼에도 과거의 평균과 표준편차를 이용하여 표준화시킨다면, 실제 가치에 대한 반영이 잘 안될 수 있다는 의미이다. 또한, 이를 현재 기준으로 평균과 표준 편차를 구하여 표준화를 했을 시에도 문제는 발생한다. 학습시킨 모델이 한 달 전에 표준화시켰던 데이터에 Fitting(적합)이 되어있기 때문에, 현재 Data에 대해서도 잘 반영이 될 수 있을지 장담할 수 없다는 것이다.
그.래.서. 표준화(Standardization)는 종가에 대한 평균이 계속 바뀌다보니, 이에 대한 변동성을 최대한 줄이고자 Min - Max Normalization을 선택했다. 물론, Min-Max Normalization에 대한 가장 큰 문제는 범위를 초과하는 가격이 있을 시, 이에 대한 Scaling이 엉망으로 된다는 점이다. 그러나, 적정한 Min-Max 범위를 잡는다면, 변동에 대한 걱정은 훨씬 덜하기 때문에 Min-Max를 크게 벗어나지 않는 이상, 한 번 학습한 모델을 지속적으로 사용할 수 있다는 장점이 있다.
다시 본론으로 돌아오면, min-max의 범위를 안정적으로 잡기 위해서 올 해 7월 부터 현재까지의 최고가, 최저가를 기준으로 잡고 Normalization을 진행했다.
dataLoader = DataLoader()
q, p = dataLoader._make_query('20200701000000', '20201013000000', 'day', 'KRW-XRP')
chart_data_day = dataLoader._run_only_query(q, p)
_max = max(chart_data_day.high)
_min = min(chart_data_day.high)
chart_data['close_norm'] = chart_data.close.apply(lambda x : (x-_min)/(_max-_min))
3. 시가, 저가, 고가 (Open, Low, High)변환
: 시가, 저가, 고가에 대한 정보도 미래에 어떠한 행동을 결정하는데, 중요한 정보인 것은 사실이다. 그러나, 코인 같은 경우에는 주식 처럼 휴장이 존재하지 않기 때문에, 가격 차트가 더욱 연속적이다. ( 종가 = 다음 시가 ) 그렇다면, 시가, 저가, 고가를 어떤 식으로 변환을 하면 좋을 지 고민해봤다.
사실 종가, 시가, 고가, 저가를 변환없이 가격 그대로를 학습 데이터로 사용할 수 있다. 그러나, Neural Network의 특성상 각 Data는 연속적인 선형 결합들에 대해 정보를 계속 변환시키기 때문에 좀 더 고민을 해 보았다. 위의 그림을 참고하여, 좀 더 과장되게 설명한다면, 신경망 학습을 시킬 때, 종가의 가격이 각각 250, 500으로 다름에도 선형적인 결합으로 인해 단순히 종가와 고가의 차이인 "30" 대한 정보만 Focusing이 될 확률이 높을 수 있다는 것이다.
하지만 본인은 종가와 고가의 차이인 30이라는 값 자체보다, 30이라는 값이 실제 종가 대비 얼만큼의 비중을 차지하는지가 앞으로의 가격 흐름에 더 중요하다고 생각했기 때문에, 시가, 저가, 고가를 종가에 대한 비율로 변환했다.
chart_data['open_close_ratio'] = (chart_data.open - chart_data.close) / chart_data.close
chart_data['low_close_ratio'] = (chart_data.low - chart_data.close) / chart_data.close
chart_data['high_close_ratio'] = (chart_data.high - chart_data.close) / chart_data.close
사실 그냥 Data 그대로 넣어도 Network 내에서 더 좋은 특징을 뽑아낼 수도 있겠지만, 굳이 종가에 대한 비율이라는 좋은 정보를 안 넣을 이유도 없기 때문에 학습 데이터에 추가해보기로 했다.
4. 거래량 변환 ( Volume Transformation )
: 거래량은 거래량 자체만으로도 모델을 학습시킬 수 있을정도로 중요한 정보이다. 가격 차트는 거래량의 발자취라고 할 정도로 중요한 변수인데, 이를 학습에 어떻게 녹여내야할 지 무척 고민스러웠다. 가장 큰 문제는 거래량의 분포를 보면 알 수 있다.
대부분의 거래량은 0.01 이하에 분포되어 있지만, 그에 비해 수 십에서 수 백배에 달하는 거래량도 함께 분포되어 있다. 이럴 경우 min-max normalization을 한다고 하더라도 대부분의 값들이 0에 가까운 값을 갖기 때문에, 이를 그대로 사용할 경우 평소의 구간에서 거래량은 매수, 매도 행동을 결정하는데 영향을 미치기 힘들게 된다.
그렇다고 Standardization을 진행한다고 하더라도 평균 값이 위와 같이 실제 분포되어 있는 값들과 비교했을 때, 상당히 괴리감이 있기 때문에 제대로된 표준화가 어려울 뿐더러, 이 데이터를 가지고 학습시킨 뒤, 위에 종가 Normalization에서 언급한 것 처럼, 현 시점 거래량에 대한 가치와 차이가 크기 때문에 적용하기가 곤란하다.
그.래.서. 최대한 거래량에 대한 정보를 살리면서 학습하기에 좋은 방향으로 변환(Transformation)하는 방향으로 생각해보았다. 실제로 Data Transformation은 통계분석을 할 때, 많이 사용된다. Normal성을 보이지 않는 Raw Data를 특정 방식으로 변환을 시킨 뒤, 분석을 진행하면 Normal성을 따르기 때문에 기존의 통계적인 기술을 적용할 수 있기 때문이다. 주의할 점은, 보통 많은 사람들이 변환한 뒤 통계적인 기술을 적용하고 원래의 방향으로 역변환시킬 때, 통계적인 기술을 적용한 결과도 역변환을 시킨 뒤, 최종 결과로 도출하는데, 이는 항상 성립하는 관계가 아니라 조정 절차가 필요하다.
아무튼 거래량 Data도 변환을 시키기로 했다. 바로 Log Transformation이다. 대단한 것처럼 보이지만, 사실 거래량 Data에 Log를 취했다고 보면된다. 그 이유는 거래량은 항상 0보다 큰 값을 갖고, 거래량이 수 백 ~ 수 천 배 이상 차이가 나는 Scale에 대해서는 Log(10)를 취할 경우, Scale의 차이를 10의 자리를 1로 Scaling 할 수 있기 때문이다. 예를 들어, log10으로 변환할 경우, 100은 2로 100,000은 5로 Scaling을 할 수 있다는 의미이다. 또한, Log 함수는 Linear하게 증가하는 함수이므로, 거래량에서 갖는 Linear한 성격도 갖고 있기 때문에, 거래량을 잘 대변해 줄 수 있을 것이라고 판단했다.
첨언하자면, 이러한 Log + Power Transformation에 General한 변형 방식이 있긴 하다. 특히, 값이 항상 0 이상인 Data에 대한 변환은 Box-Cox Transformation을 많이 이용하는데, 이번 변환에서는 이를 사용하지 않고, log(10) 변환을 이용했다. 그 이유는 Box-Cox는 Log 변환 시, 자연상수 e를 밑으로 사용하기 때문에, Scale의 Spectrum이 여전히 컸기 때문이다.
import numpy as np
volArr = np.array(chart_data.volume).reshape(-1,1)
volArr /= 10000
volArr = np.apply_along_axis(math.log10, 1, volArr)
여기서, volArr /= 10000을 한 이유는 거래량의 최소 값이 40,000 이었기 때문에 천의 자리는 보지 않기 위해 처리를 했다. 만약 위와 같이 처리하지 않을 경우, 거래량은 4 ~ 8 사이의 값을 지니겠지만, 위와 같은 처리를 할 경우 1 ~ 5 사이의 값을 지닐 수 있기 때문이다. (사실 log의 특성상 나중에 3을 빼주는 것과 동일하긴 하다)
직관적으로 비교해보면, 거래량에 대한 영향력이 w라고 했을 때, 기존의 Raw Data 같은 경우에는 Neural Network 상에서 최소 거래량 = 1*w ~ 최대 거래량 = 40,000,000*w 으로 입력이 되기 때문에 w가 거래량 자체에 무척 민감하게 학습될 것이다. 그러나 Log 변환을 했을 경우에는 최소 거래량 = 1*w ~ 최대 거래량 = 4*w 이므로 거래량에 좀 더 덜 민감하게 학습될 수 있고, 이는 w에 대한 최적점을 찾기 훨씬 수월해질 수 있을 것이다.
'Auto Trading > Pretreating' 카테고리의 다른 글
Chart Data 전처리 (6) : OBV / RSI / CMF (0) | 2020.10.27 |
---|---|
Chart Data 전처리 (5) : Stochastic Oscillator / CCI / 이격도 / Bollinger Band (0) | 2020.10.25 |
Chart Data 전처리 (4) : MACD / ROC & Momentum / DMI & ADX (0) | 2020.10.18 |
Chart Data 전처리 (3) : 지수이동평균 (EMA) (0) | 2020.10.16 |
Chart Data 전처리 (1) : Why? & How? (0) | 2020.10.10 |