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/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}")