포스트

시계열분석 Arima 예측모델에 대하여(2)

주식 자동 매매와 ML forcast 모델을 결합해서 나의 작은 재테크를 구현해보자.

1. 데이터 준비


데이터 소스 (yfinance)

yfinance를 통해 야후 주식 데이터를 크롤링하는 라이브러리를 이용하자 https://github.com/ranaroussi/yfinance

예시로 인도 nifty50 주식을 가져와보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.arima.model import ARIMA
from datetime import datetime

# 주식 데이터를 불러올 종목과 기간을 설정합니다.
# stock_symbol = 'AAPL'  # 예시로 애플 주식 데이터를 사용합니다.
stock_symbol = '453810.KS'  # 삼성 nifty50
start_date = '2024-01-01'
end_date = datetime.today().strftime('%Y-%m-%d')

# 주식 데이터를 불러옵니다.
stock_data = yf.download(stock_symbol, start=start_date, end=end_date)
 OpenHighLowCloseAdj CloseVolume
Date      
2024-01-0212030.012030.011790.011820.011797.625000283985
2024-01-0311825.011915.011820.011845.011822.577148203386
2024-01-0411845.011935.011830.011930.011907.416992148137
2024-01-0511930.012035.011930.012010.011987.265625245707
2024-01-0812070.012070.011965.011965.011942.350586403503
2024-01-0912000.012035.011920.012035.012012.217773181371
2024-01-1012035.012035.011905.011965.011942.350586278277
2024-01-1112045.012045.011980.012000.011977.284180188438
2024-01-1212025.012095.011965.012095.012072.104492203738
2024-01-1512140.012310.012125.012290.012266.735352417582
2024-01-1612590.012600.012325.012400.012376.527344463884
2024-01-1712595.012595.012195.012205.012181.896484 

2. 모델 ARIMA의 (p, d, q) 파라미터 구하기


1편에서 기술했듯이 차분을 통한 데이터 정규화 단계가 필요하다.

이후 ACF와 PACF를 구해 ARIMA 모델의 파라미터 (p, d, q)를 최종적으로 구해야한다.

우선 차분하여 완만하게 만들어주고 통계적으로 유의한지 확인해보자.

차분 횟수 d 구하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import copy

from statsmodels.tsa.stattools import adfuller


# 차분 횟수 초기화
diff_count = 0

# 정상성 테스트를 위한 함수 정의
def check_stationarity(timeseries):
    # 주어진 시계열 데이터에 대한 Dickey-Fuller 단위근 검정
    result = adfuller(timeseries)
    print('ADF 통계량: ', result[0])
    print('p-value: ', result[1])
    print('Critical Values:')
    for key, value in result[4].items():
        print('\t{}: {}'.format(key, value))
    if result[1] <= 0.05:
        print('시계열이 안정적입니다.')
    else:
        print('시계열이 안정적이지 않습니다.')

df = copy.deepcopy(stock_data)
        
# 주식 종가를 차분하여 정상성을 확인
while True:
    # 종가 차분
    diff_close = df['Close'].diff().dropna()
    diff_count += 1
    print(f'차분 횟수: {diff_count}')
    print(diff_close)
    
    # 정상성 확인
    check_stationarity(diff_close)
    
    # 그래프 그리기
    plt.figure(figsize=(10, 5))
    plt.plot(diff_close, color='blue')
    plt.title(f'Differenced Close Price (Order {diff_count})')
    plt.xlabel('Date')
    plt.ylabel('Differenced Close Price')
    plt.grid(True)
    plt.show()
    
    # 정상성이 확인되었을 때 반복문 종료
    if adfuller(diff_close)[1] <= 0.05:
        break
    
    # 차분된 데이터로 DataFrame 업데이트
    df['Close'] = diff_close

print('최종 차분 횟수: ', diff_count)

p-value가 -9승으로 매우 작고 0.05보다 낮아 통계적으로 유의하다. 그래프를 보면 고르게 분포해있다.

p와 q 구하기

종가로 acf과 pacf 도표를 그려보면 다음과 같다. 이를 보고 감소세에 들어간 부분을 찾아 주관적으로 p와 q를 기입하면 된다. 차분하고 결측값을 제외한 종가 또한 그렸는데 이는 천천히 감소하는 ACF의 경우를 보고 적당한 q 값을 찾기 어렵기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

# ACF 및 PACF 그래프 그리기
fig, ax = plt.subplots(2, 1, figsize=(10, 8))

plot_acf(stock_data['Close'], ax=ax[0])
# plot_acf(stock_data['Close'].diff().dropna(), ax=ax[0])
ax[0].set_title('Autocorrelation Function (ACF)')
ax[0].set_xlabel('Lag')
ax[0].set_ylabel('ACF')

plot_pacf(stock_data['Close'], ax=ax[1])
# plot_pacf(stock_data['Close'].diff().dropna(), ax=ax[1])
ax[1].set_title('Partial Autocorrelation Function (PACF)')
ax[1].set_xlabel('Lag')
ax[1].set_ylabel('PACF')

plt.tight_layout()
plt.show()

차분하지 않은 종가의 ACF, PACF 그래프를 살펴보자

  • ACF를 보면 q는 급격히 감소하는지는 잘 모르겠다.
  • PACF를 보면 p는 1이후로 급감하므로 1로 정할 수 있을 것 같다.

주관적으로 보았을 때 ACF이 애매하므로 차분하고 그래프를 보자면 ACF가 0 이후로 급감했다. q를 0으로 둬보자.

이런식으로 결정한 ARIMA의 p, d, q는 (1, 1, 0)이 된다.

위의 표를 보고 결정하지만 소멸하는 형태, 절단, 지수 감소 등 애매한 말이 많으므로 주관이 개입한다고 볼 수 있다.

p, d, q가 (1, 1, 0)인 조합이 괜찮은건지 모델을 fit하고 테스트해보자.

1
2
3
4
5
6
7
8
# 주식 데이터를 불러옵니다.
stock_data = yf.download(stock_symbol, start=start_date, end=end_date)
# 불러온 주식 데이터의 date인 index를 비즈니스 일자 형식으로 맞춰준다.
stock_data = stock_data.asfreq('B')

model = ARIMA(train_data, order=(1, 1, 0), freq = 'B')
fitted_model = model.fit()
fitted_model.summary()

모델 요약했을 때의 AIC 점수를 보고 판단할 수 있다. (낮을수록 좋음)

1418.645인데 사실 이게 최선이 아닐 수 있다. 더 낮은 AIC를 가진 모형이 있을 수 있다. 그렇다면 최선의 AIC 는 어떻게 구할까?

내가 구한 (p, d, q)를 의심하고 최적의 조합을 구해보기

-> 경우의 수를 구해보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from itertools import product


# Parameter search
pdq_candidates = list(product([0, 1, 2], repeat=3))

aic = []
for pdq_candidate in pdq_candidates:
    model = ARIMA(train_data, order = (pdq_candidate))
    model_fit = model.fit()
    print(f'ARIMA: {pdq_candidate} >> AIC : {round(model_fit.aic,2)}')
    aic.append(round(model_fit.aic,2))
    
# Search optimal parameters
min_aic = min(aic)
print(f"min_aic is {min_aic}")
min_idx = aic.index(min_aic)
print(f"min_idx is {min_idx}")
optimal_pdq = list(pdq_candidates)[min_idx]
optimal_pdq

내가 주관적으로 잡은 임시모델은 AIC가 1418이었는데 최적으로 찾은 조합으로는 1415가 나왔다. 큰 차이는 아니지만 최적이 아니었다 ㅋㅋ (2, 1, 2)의 조합이 최적이라고 믿고 예측까지 수행해보자.

3. 예측


예측 결과와 train / test 비율과의 관계

yfinance로 불러온 data를 train과 test로 나누고 len(test) 만큼 예측시킨다. 그리고 실제 test 결과와 얼마나 차이나는지 시각적으로 그려 확인해보자.

코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# # 테스트 세트에 대한 예측값을 생성합니다.
forecast = fitted_model.forecast(steps=len(test_data))

result = fitted_model.get_forecast(steps = len(test_data.index), alpha = 0.05).summary_frame()

fc = tuple(result['mean'].values) # 예측값
se = tuple(result['mean_se'].values) # 표준오차
lower_coef = tuple(result['mean_ci_lower'].values) # 신뢰구간 최소
upper_coef = tuple(result['mean_ci_upper'].values) # 신뢰구간 최대

# 예측값 인덱스 넣기
fc_data = pd.Series(fc, index = test_data.index)

# 신뢰구간 인덱스 넣기
lower_data = pd.Series(lower_coef, index = test_data.index)
upper_data = pd.Series(upper_coef, index = test_data.index)

#시각화 
plt.figure(figsize =(15, 6))
plt.plot(train_data, label = 'training')
plt.plot(test_data, label = 'actual')
plt.plot(fc_data, label = 'forecast')
plt.fill_between(test_data.index, lower_data, upper_data, color = 'black', alpha = 0.1)
plt.legend(loc = 'upper left')
plt.show()

train 8 : test 2

-> 처참하다. 예측보다는 줄을 그은 느낌

train 9 : test 1

-> 역시 처참하다.

train 9.5 : test 0.5

-> 마찬가지로 큰 의미는 가지지 않는 것 같다.

열심히 가설을 만들고 임시 모델을 구하고 보다 좋은 최적의 모델을 골라도 결과는 처참하다. 처음 구한 pdq(1, 1, 0)을 해보면 더 변화가 없고 별로다.

4. 결론


ARIMA 모델을 활용한 주식 종가 예측은 하면 안될 것 같다.

자동매매로 이어기를 기대하고 포스팅을 작성했으나 생각보다 더 못하기에 이걸 활용한 자동매매는 포기한다.

이 다음으로 meta의 prophet 모델을 써보고 이걸로 자동매매를 도모해보자.

참고


https://velog.io/@ljs7463/ARIMA%EB%AA%A8%EB%8D%B8%EB%A1%9C-%EC%8B%9C%EA%B3%84%EC%97%B4-%EC%98%88%EC%B8%A1%EC%B0%A8%EB%B6%84-ARMA%EC%B0%A8%EC%88%98-%EA%B5%AC%ED%95%98%EA%B8%B0

+

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.