analyser_hj3415 2.10.6__py3-none-any.whl → 3.0.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- analyser_hj3415/__init__.py +13 -0
- analyser_hj3415/analyser/__init__.py +0 -0
- analyser_hj3415/analyser/eval/__init__.py +4 -0
- analyser_hj3415/analyser/eval/blue.py +187 -0
- analyser_hj3415/analyser/eval/common.py +267 -0
- analyser_hj3415/analyser/eval/growth.py +110 -0
- analyser_hj3415/analyser/eval/mil.py +274 -0
- analyser_hj3415/analyser/eval/red.py +295 -0
- analyser_hj3415/{score.py → analyser/score.py} +24 -23
- analyser_hj3415/analyser/tsa/__init__.py +2 -0
- analyser_hj3415/analyser/tsa/lstm.py +670 -0
- analyser_hj3415/analyser/tsa/prophet.py +207 -0
- analyser_hj3415/cli.py +20 -89
- {analyser_hj3415-2.10.6.dist-info → analyser_hj3415-3.0.1.dist-info}/METADATA +3 -3
- analyser_hj3415-3.0.1.dist-info/RECORD +22 -0
- analyser_hj3415/eval.py +0 -960
- analyser_hj3415/tsa.py +0 -708
- analyser_hj3415-2.10.6.dist-info/RECORD +0 -14
- {analyser_hj3415-2.10.6.dist-info → analyser_hj3415-3.0.1.dist-info}/WHEEL +0 -0
- {analyser_hj3415-2.10.6.dist-info → analyser_hj3415-3.0.1.dist-info}/entry_points.txt +0 -0
analyser_hj3415/tsa.py
DELETED
@@ -1,708 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Time Series Analysis
|
3
|
-
"""
|
4
|
-
from pprint import pprint
|
5
|
-
|
6
|
-
import numpy as np
|
7
|
-
import yfinance as yf
|
8
|
-
from datetime import datetime, timedelta
|
9
|
-
import pandas as pd
|
10
|
-
from prophet import Prophet
|
11
|
-
from sklearn.preprocessing import StandardScaler
|
12
|
-
from utils_hj3415 import utils, helpers
|
13
|
-
from typing import Optional
|
14
|
-
import plotly.graph_objs as go
|
15
|
-
from plotly.offline import plot
|
16
|
-
import matplotlib.pyplot as plt # Matplotlib 수동 임포트
|
17
|
-
from db_hj3415 import myredis
|
18
|
-
from collections import OrderedDict
|
19
|
-
from analyser_hj3415 import eval
|
20
|
-
from sklearn.preprocessing import MinMaxScaler
|
21
|
-
from tensorflow.keras.models import Sequential
|
22
|
-
from tensorflow.keras.layers import LSTM, Dense, Dropout
|
23
|
-
from tensorflow.keras.callbacks import EarlyStopping
|
24
|
-
from tensorflow.keras import Input
|
25
|
-
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
|
26
|
-
from dataclasses import dataclass
|
27
|
-
import itertools
|
28
|
-
|
29
|
-
import logging
|
30
|
-
|
31
|
-
tsa_logger = helpers.setup_logger('tsa_logger', logging.WARNING)
|
32
|
-
|
33
|
-
|
34
|
-
class MyProphet:
|
35
|
-
def __init__(self, code: str):
|
36
|
-
assert utils.is_6digit(code), f'Invalid value : {code}'
|
37
|
-
self.scaler = StandardScaler()
|
38
|
-
|
39
|
-
self.model = Prophet()
|
40
|
-
self._code = code
|
41
|
-
self.name = myredis.Corps(code, 'c101').get_name()
|
42
|
-
self.raw_data = self._get_raw_data()
|
43
|
-
self.df_real = self._preprocessing_for_prophet()
|
44
|
-
self.df_forecast = self._make_forecast()
|
45
|
-
|
46
|
-
@property
|
47
|
-
def code(self) -> str:
|
48
|
-
return self._code
|
49
|
-
|
50
|
-
@code.setter
|
51
|
-
def code(self, code: str):
|
52
|
-
assert utils.is_6digit(code), f'Invalid value : {code}'
|
53
|
-
tsa_logger.info(f'change code : {self.code} -> {code}')
|
54
|
-
self.model = Prophet()
|
55
|
-
self._code = code
|
56
|
-
self.name = myredis.Corps(code, 'c101').get_name()
|
57
|
-
self.raw_data = self._get_raw_data()
|
58
|
-
self.df_real = self._preprocessing_for_prophet()
|
59
|
-
self.df_forecast = self._make_forecast()
|
60
|
-
|
61
|
-
@staticmethod
|
62
|
-
def is_valid_date(date_string):
|
63
|
-
try:
|
64
|
-
# %Y-%m-%d 형식으로 문자열을 datetime 객체로 변환 시도
|
65
|
-
datetime.strptime(date_string, '%Y-%m-%d')
|
66
|
-
return True
|
67
|
-
except ValueError:
|
68
|
-
# 변환이 실패하면 ValueError가 발생, 형식이 맞지 않음
|
69
|
-
return False
|
70
|
-
|
71
|
-
def _get_raw_data(self) -> pd.DataFrame:
|
72
|
-
"""
|
73
|
-
야후에서 해당 종목의 4년간 주가 raw data를 받아온다.
|
74
|
-
:return:
|
75
|
-
"""
|
76
|
-
# 오늘 날짜 가져오기
|
77
|
-
today = datetime.today()
|
78
|
-
|
79
|
-
# 4년 전 날짜 계산 (4년 = 365일 * 4)
|
80
|
-
four_years_ago = today - timedelta(days=365 * 4)
|
81
|
-
|
82
|
-
return yf.download(
|
83
|
-
self.code + '.KS',
|
84
|
-
start=four_years_ago.strftime('%Y-%m-%d'),
|
85
|
-
end=today.strftime('%Y-%m-%d')
|
86
|
-
)
|
87
|
-
|
88
|
-
def _preprocessing_for_prophet(self) -> pd.DataFrame:
|
89
|
-
"""
|
90
|
-
Prophet이 사용할 수 있도록 데이터 준비
|
91
|
-
ds는 날짜, y는 주가
|
92
|
-
:return:
|
93
|
-
"""
|
94
|
-
df = self.raw_data[['Close', 'Volume']].reset_index()
|
95
|
-
df.columns = ['ds', 'y', 'volume'] # Prophet의 형식에 맞게 열 이름 변경
|
96
|
-
|
97
|
-
# ds 열에서 타임존 제거
|
98
|
-
df['ds'] = df['ds'].dt.tz_localize(None)
|
99
|
-
|
100
|
-
# 추가 변수를 정규화
|
101
|
-
df['volume_scaled'] = self.scaler.fit_transform(df[['volume']])
|
102
|
-
tsa_logger.debug('_preprocessing_for_prophet')
|
103
|
-
tsa_logger.debug(df)
|
104
|
-
return df
|
105
|
-
|
106
|
-
def _make_forecast(self) -> pd.DataFrame:
|
107
|
-
# 정규화된 'volume_scaled' 변수를 외부 변수로 추가
|
108
|
-
self.model.add_regressor('volume_scaled')
|
109
|
-
|
110
|
-
self.model.fit(self.df_real)
|
111
|
-
|
112
|
-
# 향후 180일 동안의 주가 예측
|
113
|
-
future = self.model.make_future_dataframe(periods=180)
|
114
|
-
tsa_logger.debug('_make_forecast_future')
|
115
|
-
tsa_logger.debug(future)
|
116
|
-
|
117
|
-
# 미래 데이터에 거래량 추가 (평균 거래량을 사용해 정규화)
|
118
|
-
future_volume = pd.DataFrame({'volume': [self.raw_data['Volume'].mean()] * len(future)})
|
119
|
-
future['volume_scaled'] = self.scaler.transform(future_volume[['volume']])
|
120
|
-
|
121
|
-
forecast = self.model.predict(future)
|
122
|
-
tsa_logger.debug('_make_forecast')
|
123
|
-
tsa_logger.debug(forecast)
|
124
|
-
return forecast
|
125
|
-
|
126
|
-
def get_yhat(self) -> dict:
|
127
|
-
"""
|
128
|
-
최근 날짜의 예측데이터를 반환한다.
|
129
|
-
:return: {'ds':..., 'yhat':.., 'yhat_lower':.., 'yhat_upper':..,}
|
130
|
-
"""
|
131
|
-
df = self.df_forecast
|
132
|
-
last_real_date = self.df_real.iloc[-1]['ds']
|
133
|
-
tsa_logger.info(last_real_date)
|
134
|
-
yhat_dict = df[df['ds']==last_real_date].iloc[0][['ds', 'yhat_lower', 'yhat_upper', 'yhat']].to_dict()
|
135
|
-
tsa_logger.info(yhat_dict)
|
136
|
-
return yhat_dict
|
137
|
-
|
138
|
-
def visualization(self):
|
139
|
-
# 예측 결과 출력
|
140
|
-
print(self.df_forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())
|
141
|
-
# 예측 결과 시각화 (Matplotlib 사용)
|
142
|
-
fig = self.model.plot(self.df_forecast)
|
143
|
-
# 추세 및 계절성 시각화
|
144
|
-
fig2 = self.model.plot_components(self.df_forecast)
|
145
|
-
plt.show() # 시각화 창 띄우기
|
146
|
-
|
147
|
-
def export(self, to="str") -> Optional[str]:
|
148
|
-
"""
|
149
|
-
prophet과 plotly로 그래프를 그려서 html을 문자열로 반환
|
150
|
-
:param to: str, png, htmlfile
|
151
|
-
:return:
|
152
|
-
"""
|
153
|
-
# Plotly를 사용한 시각화
|
154
|
-
fig = go.Figure()
|
155
|
-
|
156
|
-
# 실제 데이터
|
157
|
-
fig.add_trace(go.Scatter(x=self.df_real['ds'], y=self.df_real['y'], mode='markers', name='실제주가'))
|
158
|
-
# 예측 데이터
|
159
|
-
fig.add_trace(go.Scatter(x=self.df_forecast['ds'], y=self.df_forecast['yhat'], mode='lines', name='예측치'))
|
160
|
-
|
161
|
-
# 상한/하한 구간
|
162
|
-
fig.add_trace(
|
163
|
-
go.Scatter(x=self.df_forecast['ds'], y=self.df_forecast['yhat_upper'], fill=None, mode='lines', name='상한'))
|
164
|
-
fig.add_trace(
|
165
|
-
go.Scatter(x=self.df_forecast['ds'], y=self.df_forecast['yhat_lower'], fill='tonexty', mode='lines', name='하한'))
|
166
|
-
|
167
|
-
fig.update_layout(
|
168
|
-
# title=f'{self.code} {self.name} 주가 예측 그래프(prophet)',
|
169
|
-
xaxis_title='일자',
|
170
|
-
yaxis_title='주가(원)',
|
171
|
-
xaxis = dict(
|
172
|
-
tickformat='%Y/%m', # X축을 '연/월' 형식으로 표시
|
173
|
-
),
|
174
|
-
yaxis = dict(
|
175
|
-
tickformat=".0f", # 소수점 없이 원래 숫자 표시
|
176
|
-
),
|
177
|
-
showlegend=False,
|
178
|
-
)
|
179
|
-
|
180
|
-
if to == 'str':
|
181
|
-
# 그래프 HTML로 변환 (string 형식으로 저장)
|
182
|
-
graph_html = plot(fig, output_type='div')
|
183
|
-
return graph_html
|
184
|
-
elif to == 'png':
|
185
|
-
# 그래프를 PNG 파일로 저장
|
186
|
-
fig.write_image(f"myprophet_{self.code}.png")
|
187
|
-
return None
|
188
|
-
elif to == 'htmlfile':
|
189
|
-
# 그래프를 HTML로 저장
|
190
|
-
plot(fig, filename=f'myprophet_{self.code}.html', auto_open=False)
|
191
|
-
return None
|
192
|
-
else:
|
193
|
-
Exception("to 인자가 맞지 않습니다.")
|
194
|
-
|
195
|
-
def scoring(self) -> int:
|
196
|
-
"""
|
197
|
-
prophet의 yhat_lower 예측치와 주가를 비교하여 주가가 낮으면 양의 점수를 높으면 음의 점수를 준다.
|
198
|
-
|
199
|
-
Returns:
|
200
|
-
int: The calculated score based on the deviation between the recent price
|
201
|
-
and the expected lower limit.
|
202
|
-
|
203
|
-
Parameters:
|
204
|
-
None
|
205
|
-
|
206
|
-
Raises:
|
207
|
-
AttributeError: Raised if the necessary attributes like `df_real` or methods like `get_yhat`
|
208
|
-
are not correctly set or implemented.
|
209
|
-
KeyError: Raised if the expected keys (`'yhat_lower'` or `'y'`) are not found in the data involved.
|
210
|
-
ValueError: Raised if the format of data does not conform to expected structure for calculations.
|
211
|
-
"""
|
212
|
-
last_real_data = self.df_real.iloc[-1]
|
213
|
-
recent_price = last_real_data['y']
|
214
|
-
recent_date = datetime.strftime(last_real_data['ds'], '%Y-%m-%d')
|
215
|
-
yhat_dict = self.get_yhat()
|
216
|
-
tsa_logger.info(f'recent_price: {recent_price}, yhat_dict: {yhat_dict}')
|
217
|
-
yhat_lower = int(yhat_dict['yhat_lower'])
|
218
|
-
deviation = int(eval.Tools.cal_deviation(recent_price, yhat_lower))
|
219
|
-
if recent_price > yhat_lower:
|
220
|
-
score = -deviation
|
221
|
-
else:
|
222
|
-
score = deviation
|
223
|
-
tsa_logger.info(f"{self.code}/{self.name} date: {recent_date} 가격:{recent_price} 기대하한값:{yhat_lower} 편차:{deviation} score:{score}")
|
224
|
-
return score
|
225
|
-
|
226
|
-
|
227
|
-
@dataclass
|
228
|
-
class LSTMData:
|
229
|
-
code: str
|
230
|
-
|
231
|
-
data_2d: np.ndarray
|
232
|
-
train_size: int
|
233
|
-
train_data_2d: np.ndarray
|
234
|
-
test_data_2d: np.ndarray
|
235
|
-
|
236
|
-
X_train_3d: np.ndarray
|
237
|
-
X_test_3d: np.ndarray
|
238
|
-
y_train_1d: np.ndarray
|
239
|
-
y_test_1d: np.ndarray
|
240
|
-
|
241
|
-
@dataclass
|
242
|
-
class LSTMGrade:
|
243
|
-
"""
|
244
|
-
딥러닝 모델의 학습 결과를 평가하기 위해 사용하는 데이터 클래스
|
245
|
-
"""
|
246
|
-
code: str
|
247
|
-
|
248
|
-
mean_train_prediction_2d: np.ndarray
|
249
|
-
mean_test_predictions_2d: np.ndarray
|
250
|
-
|
251
|
-
train_mse: float
|
252
|
-
train_mae: float
|
253
|
-
train_r2: float
|
254
|
-
test_mse: float
|
255
|
-
test_mae: float
|
256
|
-
test_r2: float
|
257
|
-
|
258
|
-
class MyLSTM:
|
259
|
-
"""
|
260
|
-
LSTM(Long Short-Term Memory)
|
261
|
-
"""
|
262
|
-
# 미래 몇일을 예측할 것인가?
|
263
|
-
future_days = 30
|
264
|
-
|
265
|
-
def __init__(self, code: str):
|
266
|
-
assert utils.is_6digit(code), f'Invalid value : {code}'
|
267
|
-
self._code = code
|
268
|
-
self.name = myredis.Corps(code, 'c101').get_name()
|
269
|
-
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
270
|
-
self.raw_data = self._get_raw_data()
|
271
|
-
self.lstm_data = self._preprocessing_for_lstm()
|
272
|
-
|
273
|
-
@property
|
274
|
-
def code(self) -> str:
|
275
|
-
return self._code
|
276
|
-
|
277
|
-
@code.setter
|
278
|
-
def code(self, code: str):
|
279
|
-
assert utils.is_6digit(code), f'Invalid value : {code}'
|
280
|
-
tsa_logger.debug(f'change code : {self.code} -> {code}')
|
281
|
-
|
282
|
-
self._code = code
|
283
|
-
self.name = myredis.Corps(code, 'c101').get_name()
|
284
|
-
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
285
|
-
self.raw_data = self._get_raw_data()
|
286
|
-
self.lstm_data = self._preprocessing_for_lstm()
|
287
|
-
|
288
|
-
def _get_raw_data(self) -> pd.DataFrame:
|
289
|
-
"""
|
290
|
-
야후에서 해당 종목의 4년간 주가 raw data를 받아온다.
|
291
|
-
:return:
|
292
|
-
"""
|
293
|
-
# 오늘 날짜 가져오기
|
294
|
-
today = datetime.today()
|
295
|
-
|
296
|
-
# 4년 전 날짜 계산 (4년 = 365일 * 4)
|
297
|
-
four_years_ago = today - timedelta(days=365 * 4)
|
298
|
-
tsa_logger.info(f"start: {four_years_ago.strftime('%Y-%m-%d')}, end: {today.strftime('%Y-%m-%d')}")
|
299
|
-
|
300
|
-
df = yf.download(
|
301
|
-
self.code + '.KS',
|
302
|
-
start=four_years_ago.strftime('%Y-%m-%d'),
|
303
|
-
end=today.strftime('%Y-%m-%d')
|
304
|
-
)
|
305
|
-
df.index = df.index.tz_localize(None)
|
306
|
-
tsa_logger.debug(df)
|
307
|
-
return df
|
308
|
-
|
309
|
-
def _preprocessing_for_lstm(self) -> LSTMData:
|
310
|
-
"""
|
311
|
-
lstm이 사용할 수 있도록 데이터 준비(정규화 및 8:2 훈련데이터 검증데이터 분리 및 차원변환)
|
312
|
-
:return:
|
313
|
-
"""
|
314
|
-
# 필요한 열만 선택 (종가만 사용) - 2차웜 배열로 변환
|
315
|
-
data_2d = self.raw_data['Close'].values.reshape(-1, 1)
|
316
|
-
tsa_logger.debug(data_2d)
|
317
|
-
|
318
|
-
# 데이터 정규화 (0과 1 사이로 스케일링)
|
319
|
-
scaled_data_2d = self.scaler.fit_transform(data_2d)
|
320
|
-
|
321
|
-
# 학습 데이터 생성
|
322
|
-
# 주가 데이터를 80%는 학습용, 20%는 테스트용으로 분리하는 코드
|
323
|
-
train_size = int(len(scaled_data_2d) * 0.8)
|
324
|
-
train_data_2d = scaled_data_2d[:train_size]
|
325
|
-
test_data_2d = scaled_data_2d[train_size:]
|
326
|
-
tsa_logger.info(f'총 {len(data_2d)}개 데이터, train size : {train_size}')
|
327
|
-
|
328
|
-
# 학습 데이터에 대한 입력(X)과 정답(y)를 생성
|
329
|
-
def create_dataset(data, time_step=60):
|
330
|
-
X, y = [], []
|
331
|
-
for i in range(len(data) - time_step):
|
332
|
-
X.append(data[i:i + time_step, 0])
|
333
|
-
y.append(data[i + time_step, 0])
|
334
|
-
return np.array(X), np.array(y)
|
335
|
-
|
336
|
-
|
337
|
-
X_train, y_train_1d = create_dataset(train_data_2d)
|
338
|
-
X_test, y_test_1d = create_dataset(test_data_2d)
|
339
|
-
tsa_logger.debug(X_train.shape)
|
340
|
-
tsa_logger.debug(X_test.shape)
|
341
|
-
|
342
|
-
try:
|
343
|
-
# LSTM 모델 입력을 위해 데이터를 3차원으로 변환
|
344
|
-
X_train_3d = X_train.reshape(X_train.shape[0], X_train.shape[1], 1)
|
345
|
-
X_test_3d = X_test.reshape(X_test.shape[0], X_test.shape[1], 1)
|
346
|
-
except IndexError:
|
347
|
-
return LSTMData(
|
348
|
-
code=self.code,
|
349
|
-
data_2d=np.array([]),
|
350
|
-
train_size=0,
|
351
|
-
train_data_2d=np.array([]),
|
352
|
-
test_data_2d=np.array([]),
|
353
|
-
X_train_3d=np.array([]),
|
354
|
-
X_test_3d=np.array([]),
|
355
|
-
y_train_1d=np.array([]),
|
356
|
-
y_test_1d=np.array([]),
|
357
|
-
)
|
358
|
-
|
359
|
-
tsa_logger.debug(f'n_dim - X_train_3d : {X_train_3d.ndim}, X_test_3d : {X_test_3d.ndim}, y_train : {y_train_1d.ndim}, y_test : {y_test_1d.ndim}')
|
360
|
-
tsa_logger.debug(f'len - X_train_3d : {len(X_train_3d)}, X_test_3d : {len(X_test_3d)}, y_train : {len(y_train_1d)}, y_test : {len(y_test_1d)}')
|
361
|
-
|
362
|
-
return LSTMData(
|
363
|
-
code=self.code,
|
364
|
-
data_2d=data_2d,
|
365
|
-
train_size=train_size,
|
366
|
-
train_data_2d=train_data_2d,
|
367
|
-
test_data_2d=test_data_2d,
|
368
|
-
X_train_3d=X_train_3d,
|
369
|
-
X_test_3d=X_test_3d,
|
370
|
-
y_train_1d=y_train_1d,
|
371
|
-
y_test_1d=y_test_1d,
|
372
|
-
)
|
373
|
-
|
374
|
-
def _model_training(self) -> Sequential:
|
375
|
-
# LSTM 모델 생성 - 유닛과 드롭아웃의 수는 테스트로 최적화 됨.
|
376
|
-
model = Sequential()
|
377
|
-
# Input(shape=(50, 1))는 50개의 타임스텝을 가지는 입력 데이터를 처리하며, 각 타임스텝에 1개의 특성이 있다는 것을 의미
|
378
|
-
model.add(Input(shape=(self.lstm_data.X_train_3d.shape[1], 1))) # 입력 레이어에 명시적으로 Input을 사용
|
379
|
-
model.add(LSTM(units=150, return_sequences=True))
|
380
|
-
model.add(Dropout(0.2))
|
381
|
-
model.add(LSTM(units=75, return_sequences=False))
|
382
|
-
model.add(Dropout(0.2))
|
383
|
-
model.add(Dense(units=25))
|
384
|
-
model.add(Dropout(0.3))
|
385
|
-
model.add(Dense(units=1))
|
386
|
-
|
387
|
-
# 모델 요약 출력
|
388
|
-
# model.summary()
|
389
|
-
|
390
|
-
# 모델 컴파일 및 학습
|
391
|
-
model.compile(optimizer='adam', loss='mean_squared_error')
|
392
|
-
|
393
|
-
# 조기 종료 설정
|
394
|
-
early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)
|
395
|
-
|
396
|
-
# 모델 학습 - 과적합 방지위한 조기종료 세팅
|
397
|
-
model.fit(self.lstm_data.X_train_3d, self.lstm_data.y_train_1d,
|
398
|
-
epochs=75, batch_size=32, validation_data=(self.lstm_data.X_test_3d, self.lstm_data.y_test_1d),
|
399
|
-
callbacks=[early_stopping])
|
400
|
-
return model
|
401
|
-
|
402
|
-
def ensemble_training(self, num) -> tuple:
|
403
|
-
"""
|
404
|
-
딥러닝을 num 회 반복하고 평균을 사용하는 함수
|
405
|
-
:param num: 앙상블 모델 수
|
406
|
-
:return:
|
407
|
-
"""
|
408
|
-
def prediction(model_in: Sequential, data: np.ndarray) -> np.ndarray:
|
409
|
-
"""
|
410
|
-
훈련될 모델을 통해 예측을 시행하여 정규화를 복원하고 결과 반환한다.
|
411
|
-
:param model_in:
|
412
|
-
:param data:
|
413
|
-
:return:
|
414
|
-
"""
|
415
|
-
predictions_2d = model_in.predict(data)
|
416
|
-
predictions_scaled_2d = self.scaler.inverse_transform(predictions_2d) # 스케일링 복원
|
417
|
-
tsa_logger.info(f'predictions_scaled_2d : ndim - {predictions_scaled_2d.ndim} len - {len(predictions_scaled_2d)}') # numpy.ndarray 타입
|
418
|
-
tsa_logger.debug(predictions_scaled_2d)
|
419
|
-
return predictions_scaled_2d
|
420
|
-
|
421
|
-
ensemble_train_predictions_2d = []
|
422
|
-
ensemble_test_predictions_2d = []
|
423
|
-
ensemble_future_predictions_2d = []
|
424
|
-
|
425
|
-
for i in range(num):
|
426
|
-
print(f"Training model {i + 1}/{num}...")
|
427
|
-
model = self._model_training()
|
428
|
-
|
429
|
-
# 훈련 데이터 예측
|
430
|
-
train_predictions_scaled_2d = prediction(model, self.lstm_data.X_train_3d)
|
431
|
-
ensemble_train_predictions_2d.append(train_predictions_scaled_2d)
|
432
|
-
|
433
|
-
# 테스트 데이터 예측
|
434
|
-
test_predictions_scaled_2d = prediction(model, self.lstm_data.X_test_3d)
|
435
|
-
ensemble_test_predictions_2d.append(test_predictions_scaled_2d)
|
436
|
-
|
437
|
-
# 8. 미래 30일 예측
|
438
|
-
# 마지막 60일간의 데이터를 기반으로 미래 30일을 예측
|
439
|
-
|
440
|
-
last_60_days_2d = self.lstm_data.test_data_2d[-60:]
|
441
|
-
last_60_days_3d = last_60_days_2d.reshape(1, -1, 1)
|
442
|
-
|
443
|
-
future_predictions = []
|
444
|
-
for _ in range(self.future_days):
|
445
|
-
predicted_price_2d = model.predict(last_60_days_3d)
|
446
|
-
future_predictions.append(predicted_price_2d[0][0])
|
447
|
-
|
448
|
-
# 예측값을 다시 입력으로 사용하여 새로운 예측을 만듦
|
449
|
-
predicted_price_reshaped = np.reshape(predicted_price_2d, (1, 1, 1)) # 3D 배열로 변환
|
450
|
-
last_60_days_3d = np.append(last_60_days_3d[:, 1:, :], predicted_price_reshaped, axis=1)
|
451
|
-
|
452
|
-
# 예측된 주가를 다시 스케일링 복원
|
453
|
-
future_predictions_2d = np.array(future_predictions).reshape(-1, 1)
|
454
|
-
future_predictions_scaled_2d = self.scaler.inverse_transform(future_predictions_2d)
|
455
|
-
ensemble_future_predictions_2d.append(future_predictions_scaled_2d)
|
456
|
-
|
457
|
-
return ensemble_train_predictions_2d, ensemble_test_predictions_2d, ensemble_future_predictions_2d
|
458
|
-
|
459
|
-
def grading(self, ensemble_train_predictions_2d: list, ensemble_test_predictions_2d: list) -> LSTMGrade:
|
460
|
-
"""
|
461
|
-
딥러닝 결과를 분석하기 위한 함수
|
462
|
-
:param ensemble_train_predictions_2d:
|
463
|
-
:param ensemble_test_predictions_2d:
|
464
|
-
:return:
|
465
|
-
"""
|
466
|
-
# 예측값을 평균내서 최종 예측값 도출
|
467
|
-
mean_train_prediction_2d = np.mean(ensemble_train_predictions_2d, axis=0)
|
468
|
-
mean_test_predictions_2d = np.mean(ensemble_test_predictions_2d, axis=0)
|
469
|
-
|
470
|
-
# y값(정답) 정규화 해제
|
471
|
-
y_train_scaled_2d = self.scaler.inverse_transform(self.lstm_data.y_train_1d.reshape(-1, 1))
|
472
|
-
y_test_scaled_2d = self.scaler.inverse_transform(self.lstm_data.y_test_1d.reshape(-1, 1))
|
473
|
-
|
474
|
-
# 평가 지표 계산
|
475
|
-
train_mse = mean_squared_error(y_train_scaled_2d, mean_train_prediction_2d)
|
476
|
-
train_mae = mean_absolute_error(y_train_scaled_2d, mean_train_prediction_2d)
|
477
|
-
train_r2 = r2_score(y_train_scaled_2d, mean_train_prediction_2d)
|
478
|
-
|
479
|
-
test_mse = mean_squared_error(y_test_scaled_2d, mean_test_predictions_2d)
|
480
|
-
test_mae = mean_absolute_error(y_test_scaled_2d, mean_test_predictions_2d)
|
481
|
-
test_r2 = r2_score(y_test_scaled_2d, mean_test_predictions_2d)
|
482
|
-
|
483
|
-
# 평가 결과 출력
|
484
|
-
print("Training Data:")
|
485
|
-
print(f"Train MSE: {train_mse}, Train MAE: {train_mae}, Train R²: {train_r2}")
|
486
|
-
print("\nTesting Data:")
|
487
|
-
print(f"Test MSE: {test_mse}, Test MAE: {test_mae}, Test R²: {test_r2}")
|
488
|
-
# mse, mae는 작을수록 좋으며 R^2은 0-1 사이값 1에 가까울수록 정확함
|
489
|
-
# 과적합에 대한 평가는 train 과 test를 비교하여 test가 너무 않좋으면 과적합 의심.
|
490
|
-
|
491
|
-
return LSTMGrade(
|
492
|
-
code=self.code,
|
493
|
-
mean_train_prediction_2d=mean_train_prediction_2d,
|
494
|
-
mean_test_predictions_2d=mean_test_predictions_2d,
|
495
|
-
train_mse=train_mse,
|
496
|
-
train_mae=train_mae,
|
497
|
-
train_r2=train_r2,
|
498
|
-
test_mse=test_mse,
|
499
|
-
test_mae=test_mae,
|
500
|
-
test_r2=test_r2,
|
501
|
-
)
|
502
|
-
|
503
|
-
def get_final_predictions(self, refresh: bool, expire_time_h: int, num=5) -> tuple:
|
504
|
-
"""
|
505
|
-
미래 예측치를 레디스 캐시를 이용하여 반환함
|
506
|
-
:param refresh:
|
507
|
-
:param num: 앙상블 반복횟수
|
508
|
-
:return:
|
509
|
-
"""
|
510
|
-
print("**** Start get_final_predictions... ****")
|
511
|
-
redis_name = f'{self.code}_mylstm_predictions'
|
512
|
-
|
513
|
-
print(
|
514
|
-
f"redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time_h}h")
|
515
|
-
|
516
|
-
def fetch_final_predictions(num_in) -> tuple:
|
517
|
-
"""
|
518
|
-
앙상블법으로 딥러닝을 모델을 반복해서 평균을 내서 미래를 예측한다. 평가는 래시스 캐시로 반환하기 어려워 일단 디버그 용도로만 사용하기로
|
519
|
-
:param num_in:
|
520
|
-
:return:
|
521
|
-
"""
|
522
|
-
# 앙상블 테스트와 채점
|
523
|
-
try:
|
524
|
-
_, _, ensemble_future_predictions_2d = self.ensemble_training(
|
525
|
-
num=num_in)
|
526
|
-
except IndexError:
|
527
|
-
return [], []
|
528
|
-
|
529
|
-
"""if grading:
|
530
|
-
lstm_grade = self.grading(ensemble_train_predictions_2d, ensemble_test_predictions_2d)
|
531
|
-
else:
|
532
|
-
lstm_grade = None"""
|
533
|
-
|
534
|
-
# 시각화를 위한 준비 - 날짜 생성 (미래 예측 날짜), 미래예측값 평균
|
535
|
-
last_date = self.raw_data.index[-1]
|
536
|
-
future_dates = pd.date_range(last_date, periods=self.future_days + 1).tolist()[1:]
|
537
|
-
|
538
|
-
# Timestamp 객체를 문자열로 변환
|
539
|
-
future_dates_str= [date.strftime('%Y-%m-%d') for date in future_dates]
|
540
|
-
|
541
|
-
final_future_predictions = np.mean(ensemble_future_predictions_2d, axis=0)
|
542
|
-
tsa_logger.info(f'num - future dates : {len(future_dates_str)} future data : {len(final_future_predictions)}')
|
543
|
-
|
544
|
-
assert len(future_dates_str) == len(final_future_predictions), "future_dates 와 final_future_predictions 개수가 일치하지 않습니다."
|
545
|
-
|
546
|
-
return future_dates_str, final_future_predictions.tolist()
|
547
|
-
|
548
|
-
future_dates_str, final_future_predictions = myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_final_predictions, num, timer=expire_time_h * 3600)
|
549
|
-
|
550
|
-
# 문자열을 날짜 형식으로 변환
|
551
|
-
future_dates = [datetime.strptime(date, '%Y-%m-%d') for date in future_dates_str]
|
552
|
-
|
553
|
-
# 리스트를 다시 NumPy 배열로 변환
|
554
|
-
final_future_predictions = np.array(final_future_predictions)
|
555
|
-
|
556
|
-
return future_dates, final_future_predictions
|
557
|
-
|
558
|
-
def export(self, refresh=False, expire_time_h=24, to="str") -> Optional[str]:
|
559
|
-
"""
|
560
|
-
prophet과 plotly로 그래프를 그려서 html을 문자열로 반환
|
561
|
-
:param refresh:
|
562
|
-
:param to: str, htmlfile, png
|
563
|
-
:return:
|
564
|
-
"""
|
565
|
-
future_dates, final_future_predictions = self.get_final_predictions(refresh=refresh, expire_time_h=expire_time_h)
|
566
|
-
final_future_predictions = final_future_predictions.reshape(-1) # 차원을 하나 줄인다.
|
567
|
-
|
568
|
-
# 데이터 준비
|
569
|
-
self.raw_data = self.raw_data.reset_index()
|
570
|
-
data = self.raw_data[['Date', 'Close']][-120:].reset_index(drop=True)
|
571
|
-
|
572
|
-
# 'Date'와 'Close' 열 추출
|
573
|
-
actual_dates = pd.to_datetime(data['Date'])
|
574
|
-
actual_close = data['Close']
|
575
|
-
|
576
|
-
# 'actual_close'가 Series인지 확인
|
577
|
-
if isinstance(actual_close, pd.DataFrame):
|
578
|
-
actual_close = actual_close.squeeze()
|
579
|
-
|
580
|
-
# 'Close' 열의 데이터 타입 확인
|
581
|
-
actual_close = actual_close.astype(float)
|
582
|
-
|
583
|
-
# 예측 데이터 준비
|
584
|
-
predicted_dates = pd.to_datetime(future_dates)
|
585
|
-
predicted_close = pd.Series(final_future_predictions, index=range(len(final_future_predictions))).astype(float)
|
586
|
-
|
587
|
-
# 그래프 생성
|
588
|
-
fig = go.Figure()
|
589
|
-
|
590
|
-
# 실제 데이터 추가
|
591
|
-
fig.add_trace(go.Scatter(
|
592
|
-
x=actual_dates,
|
593
|
-
y=actual_close,
|
594
|
-
mode='markers',
|
595
|
-
name='실제주가'
|
596
|
-
))
|
597
|
-
|
598
|
-
# 예측 데이터 추가
|
599
|
-
fig.add_trace(go.Scatter(
|
600
|
-
x=predicted_dates,
|
601
|
-
y=predicted_close,
|
602
|
-
mode='lines+markers',
|
603
|
-
name='예측치(30일)'
|
604
|
-
))
|
605
|
-
|
606
|
-
# 레이아웃 업데이트
|
607
|
-
fig.update_layout(
|
608
|
-
xaxis_title='일자',
|
609
|
-
yaxis_title='주가(원)',
|
610
|
-
xaxis=dict(
|
611
|
-
tickformat='%Y/%m',
|
612
|
-
),
|
613
|
-
yaxis=dict(
|
614
|
-
tickformat=".0f",
|
615
|
-
),
|
616
|
-
showlegend=True,
|
617
|
-
)
|
618
|
-
|
619
|
-
tsa_logger.debug(f"actual_dates({len(actual_dates)}) - {actual_dates}")
|
620
|
-
tsa_logger.debug(f"actual_close({len(actual_close)} - {actual_close}")
|
621
|
-
tsa_logger.debug(f"predicted_dates({len(future_dates)}) - {future_dates}")
|
622
|
-
tsa_logger.debug(f"predicted_close({len(predicted_close)}) - {predicted_close}")
|
623
|
-
|
624
|
-
fig.update_layout(
|
625
|
-
# title=f'{self.code} {self.name} 주가 예측 그래프(prophet)',
|
626
|
-
xaxis_title='일자',
|
627
|
-
yaxis_title='주가(원)',
|
628
|
-
xaxis = dict(
|
629
|
-
tickformat='%Y/%m', # X축을 '연/월' 형식으로 표시
|
630
|
-
),
|
631
|
-
yaxis = dict(
|
632
|
-
tickformat=".0f", # 소수점 없이 원래 숫자 표시
|
633
|
-
),
|
634
|
-
showlegend=False,
|
635
|
-
)
|
636
|
-
|
637
|
-
if to == 'str':
|
638
|
-
# 그래프 HTML로 변환 (string 형식으로 저장)
|
639
|
-
graph_html = plot(fig, output_type='div')
|
640
|
-
return graph_html
|
641
|
-
elif to == 'png':
|
642
|
-
# 그래프를 PNG 파일로 저장
|
643
|
-
fig.write_image(f"myLSTM_{self.code}.png")
|
644
|
-
return None
|
645
|
-
elif to == 'htmlfile':
|
646
|
-
# 그래프를 HTML로 저장
|
647
|
-
plot(fig, filename=f'myLSTM_{self.code}.html', auto_open=False)
|
648
|
-
return None
|
649
|
-
else:
|
650
|
-
Exception("to 인자가 맞지 않습니다.")
|
651
|
-
|
652
|
-
def visualization(self, refresh=True):
|
653
|
-
future_dates, final_future_predictions = self.get_final_predictions(refresh=refresh, expire_time_h=1)
|
654
|
-
|
655
|
-
# 시각화1
|
656
|
-
plt.figure(figsize=(10, 6))
|
657
|
-
|
658
|
-
# 실제 주가
|
659
|
-
plt.plot(self.raw_data.index, self.raw_data['Close'], label='Actual Price')
|
660
|
-
|
661
|
-
# 미래 주가 예측
|
662
|
-
plt.plot(future_dates, final_future_predictions, label='Future Predicted Price', linestyle='--')
|
663
|
-
|
664
|
-
plt.xlabel('Date')
|
665
|
-
plt.ylabel('Stock Price')
|
666
|
-
plt.legend()
|
667
|
-
plt.title('Apple Stock Price Prediction with LSTM')
|
668
|
-
plt.show()
|
669
|
-
|
670
|
-
"""# 시각화2
|
671
|
-
plt.figure(figsize=(10, 6))
|
672
|
-
plt.plot(self.raw_data.index[self.lstm_data.train_size + 60:], self.lstm_data.data_2d[self.lstm_data.train_size + 60:], label='Actual Price')
|
673
|
-
plt.plot(self.raw_data.index[self.lstm_data.train_size + 60:], lstm_grade.mean_test_predictions_2d, label='Predicted Price')
|
674
|
-
plt.xlabel('Date')
|
675
|
-
plt.ylabel('Price')
|
676
|
-
plt.legend()
|
677
|
-
plt.title('Stock Price Prediction with LSTM Ensemble')
|
678
|
-
plt.show()"""
|
679
|
-
|
680
|
-
def is_up(self)-> bool:
|
681
|
-
"""
|
682
|
-
lstm 데이터가 증가하는 추세인지 확인후 참/거짓 반환
|
683
|
-
|
684
|
-
Returns:
|
685
|
-
bool: True if the data is strictly increasing, False otherwise.
|
686
|
-
"""
|
687
|
-
# 튜플의 [0]은 날짜 [1]은 값 배열
|
688
|
-
data = self.get_final_predictions(refresh=False, expire_time_h=24)[1]
|
689
|
-
# 데이터를 1D 배열로 변환
|
690
|
-
flattened_data = data.flatten()
|
691
|
-
tsa_logger.debug(f"flattened_data : {flattened_data}")
|
692
|
-
# 증가 여부 확인
|
693
|
-
return all(flattened_data[i] < flattened_data[i + 1] for i in range(len(flattened_data) - 1))
|
694
|
-
|
695
|
-
@staticmethod
|
696
|
-
def caching_based_on_prophet_ranking(refresh: bool, expire_time_h: int, top=20):
|
697
|
-
ranking_topn = MyProphet.ranking(refresh=False, top=top)
|
698
|
-
tsa_logger.info(ranking_topn)
|
699
|
-
mylstm = MyLSTM('005930')
|
700
|
-
print(f"*** LSTM prediction redis cashing top{top} items ***")
|
701
|
-
for i, (code, _) in enumerate(ranking_topn.items()):
|
702
|
-
mylstm.code = code
|
703
|
-
print(f"{i+1}. {mylstm.code}/{mylstm.name}")
|
704
|
-
mylstm.get_final_predictions(refresh=refresh, expire_time_h=expire_time_h, num=5)
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|