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