analyser_hj3415 2.10.6__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
-
|