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