analyser_hj3415 3.4.2__py3-none-any.whl → 4.0.0__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/analyser/eval/red.py +66 -24
- analyser_hj3415/analyser/tsa/lstm.py +138 -169
- analyser_hj3415/analyser/tsa/prophet.py +172 -156
- analyser_hj3415/cli.py +27 -44
- {analyser_hj3415-3.4.2.dist-info → analyser_hj3415-4.0.0.dist-info}/METADATA +1 -1
- analyser_hj3415-4.0.0.dist-info/RECORD +17 -0
- analyser_hj3415/analyser/compile.py +0 -355
- analyser_hj3415/workroom/__init__.py +0 -0
- analyser_hj3415/workroom/mysklearn.py +0 -50
- analyser_hj3415/workroom/mysklearn2.py +0 -39
- analyser_hj3415/workroom/score.py +0 -342
- analyser_hj3415/workroom/trash.py +0 -289
- analyser_hj3415-3.4.2.dist-info/RECORD +0 -23
- {analyser_hj3415-3.4.2.dist-info → analyser_hj3415-4.0.0.dist-info}/WHEEL +0 -0
- {analyser_hj3415-3.4.2.dist-info → analyser_hj3415-4.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,9 @@
|
|
1
1
|
import os
|
2
|
+
from collections import OrderedDict
|
2
3
|
from dataclasses import dataclass
|
3
|
-
from typing import Tuple
|
4
|
+
from typing import Tuple, Dict, List
|
4
5
|
import math
|
6
|
+
import pickle
|
5
7
|
|
6
8
|
from utils_hj3415 import tools, setup_logger
|
7
9
|
from db_hj3415 import myredis
|
@@ -9,7 +11,7 @@ from db_hj3415 import myredis
|
|
9
11
|
from analyser_hj3415.analyser.eval.common import Tools
|
10
12
|
|
11
13
|
|
12
|
-
mylogger = setup_logger(__name__,'
|
14
|
+
mylogger = setup_logger(__name__,'INFO')
|
13
15
|
expire_time = tools.to_int(os.getenv('DEFAULT_EXPIRE_TIME_H', 48)) * 3600
|
14
16
|
|
15
17
|
|
@@ -87,6 +89,7 @@ class Red:
|
|
87
89
|
recent_price (float): 최근 주가.
|
88
90
|
expect_earn (float): 기대수익률. 기본값은 0.06 (6%).
|
89
91
|
"""
|
92
|
+
REDIS_RED_DATA_SUFFIX = "red_data"
|
90
93
|
|
91
94
|
def __init__(self, code: str, expect_earn: float = 0.06):
|
92
95
|
assert tools.is_6digit(code), f'Invalid value : {code}'
|
@@ -137,7 +140,7 @@ class Red:
|
|
137
140
|
로그:
|
138
141
|
- 비유동부채 데이터가 없을 경우 경고 메시지를 출력합니다.
|
139
142
|
"""
|
140
|
-
mylogger.
|
143
|
+
mylogger.debug(f'In the calc비유동부채... refresh : {refresh}')
|
141
144
|
self.c103.page = 'c103재무상태표q'
|
142
145
|
|
143
146
|
d, 비유동부채 = self.c103.sum_recent_4q('비유동부채', refresh)
|
@@ -264,30 +267,69 @@ class Red:
|
|
264
267
|
score = score,
|
265
268
|
)
|
266
269
|
|
267
|
-
def get(self, refresh = False
|
268
|
-
""
|
269
|
-
|
270
|
-
|
271
|
-
캐시에서 데이터를 검색하고, 없을 경우 `_generate_data`를 호출하여 새로운 데이터를 생성한 후,
|
272
|
-
캐시에 저장합니다.
|
273
|
-
|
274
|
-
매개변수:
|
275
|
-
refresh (bool, optional): 캐시 데이터를 무시하고 새로 계산 여부. 기본값은 False.
|
276
|
-
verbose (bool, optional): 실행 중 상세 정보를 출력 여부. 기본값은 True.
|
277
|
-
|
278
|
-
반환값:
|
279
|
-
RedData: Redis 캐시에서 가져오거나 새로 생성된 RedData 객체.
|
280
|
-
|
281
|
-
로그:
|
282
|
-
- 캐시 검색 상태와 새로 계산된 데이터를 출력합니다.
|
283
|
-
"""
|
284
|
-
redis_name = f"{self.code}_red_data"
|
285
|
-
mylogger.info(f"{self} RedData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
|
286
|
-
if verbose:
|
287
|
-
print(f"{self} redisname: '{redis_name}' / expect_earn: {self.expect_earn} / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
270
|
+
def get(self, refresh = False) -> RedData:
|
271
|
+
mylogger.info(f"*** Get red data ***")
|
272
|
+
redis_name = f"{self.code}_{self.REDIS_RED_DATA_SUFFIX}_{self.expect_earn}"
|
273
|
+
mylogger.info(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
288
274
|
|
289
275
|
def fetch_generate_data(refresh_in: bool) -> RedData:
|
290
276
|
return self._generate_data(refresh_in) # type: ignore
|
291
277
|
|
292
278
|
return myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_data, refresh, timer=expire_time)
|
293
279
|
|
280
|
+
@classmethod
|
281
|
+
def bulk_generate_data(cls, codes: List[str], expect_earn: float, refresh: bool) -> Dict[str, RedData]:
|
282
|
+
# --- (1) 파이프라인 GET ---
|
283
|
+
pipe = myredis.Base.redis_client.pipeline()
|
284
|
+
redis_keys = [f"{code}_{cls.REDIS_RED_DATA_SUFFIX}_{expect_earn}" for code in codes]
|
285
|
+
for redis_key in redis_keys:
|
286
|
+
pipe.get(redis_key)
|
287
|
+
results_from_redis = pipe.execute() # [val1, val2, ...]
|
288
|
+
|
289
|
+
final_results = {}
|
290
|
+
missing_codes = []
|
291
|
+
|
292
|
+
# refresh=True 이면 기존 데이터 무시하고 다시 계산해야 하므로 모두 missing 처리
|
293
|
+
if refresh:
|
294
|
+
missing_codes = codes[:]
|
295
|
+
else:
|
296
|
+
# refresh=False 이면, Redis 값이 None인 티커만 다시 계산
|
297
|
+
for code, val in zip(codes, results_from_redis):
|
298
|
+
if val is None:
|
299
|
+
missing_codes.append(code)
|
300
|
+
else:
|
301
|
+
# Redis에 pickled 데이터가 있다면 언피클해서 담기
|
302
|
+
red_data = pickle.loads(val)
|
303
|
+
final_results[code] = red_data
|
304
|
+
|
305
|
+
# --- (2) 필요한 티커만 직접 연산 ---
|
306
|
+
newly_computed_data = {}
|
307
|
+
for code in missing_codes:
|
308
|
+
mylogger.debug(f"*** bulk_generate_data : {code}")
|
309
|
+
data = cls(code, expect_earn)._generate_data(refresh=refresh)
|
310
|
+
newly_computed_data[code] = data
|
311
|
+
|
312
|
+
# --- (3) 파이프라인 SET ---
|
313
|
+
if newly_computed_data:
|
314
|
+
pipe = myredis.Base.redis_client.pipeline()
|
315
|
+
for code, data in newly_computed_data.items():
|
316
|
+
redis_key = f"{code}_{cls.REDIS_RED_DATA_SUFFIX}_{expect_earn}"
|
317
|
+
# ProphetLatestData 객체를 pickle로 직렬화
|
318
|
+
pickled_data = pickle.dumps(data)
|
319
|
+
# SET + expire_time
|
320
|
+
pipe.setex(redis_key, expire_time, pickled_data)
|
321
|
+
pipe.execute()
|
322
|
+
|
323
|
+
# 최종 결과 딕셔너리 (캐시에 있었던 것 + 새로 만든 것)
|
324
|
+
final_results.update(newly_computed_data)
|
325
|
+
return final_results
|
326
|
+
|
327
|
+
@staticmethod
|
328
|
+
def ranking(expect_earn: float = 0.06, refresh=False) -> OrderedDict:
|
329
|
+
mylogger.info("**** Start red ranking ... ****")
|
330
|
+
|
331
|
+
data = Red.bulk_generate_data(myredis.Corps.list_all_codes(), expect_earn, refresh)
|
332
|
+
mylogger.debug(data)
|
333
|
+
return OrderedDict(sorted(data.items(), key=lambda x: x[1].score, reverse=True))
|
334
|
+
|
335
|
+
|
@@ -1,12 +1,10 @@
|
|
1
1
|
import os
|
2
2
|
import numpy as np
|
3
|
+
import pandas
|
3
4
|
import yfinance as yf
|
4
5
|
from datetime import datetime, timedelta
|
5
6
|
import pandas as pd
|
6
|
-
from typing import
|
7
|
-
import plotly.graph_objs as go
|
8
|
-
from plotly.offline import plot
|
9
|
-
import matplotlib.pyplot as plt # Matplotlib 수동 임포트
|
7
|
+
from typing import Tuple, Dict, List
|
10
8
|
from sklearn.preprocessing import MinMaxScaler
|
11
9
|
from tensorflow.keras.models import Sequential # type: ignore
|
12
10
|
from tensorflow.keras.layers import LSTM, Dense, Dropout # type: ignore
|
@@ -19,7 +17,7 @@ from utils_hj3415 import tools, setup_logger
|
|
19
17
|
from db_hj3415 import myredis
|
20
18
|
from analyser_hj3415.analyser import MIs, tsa
|
21
19
|
|
22
|
-
mylogger = setup_logger(__name__,'
|
20
|
+
mylogger = setup_logger(__name__,'INFO')
|
23
21
|
expire_time = tools.to_int(os.getenv('DEFAULT_EXPIRE_TIME_H', 48)) * 3600
|
24
22
|
|
25
23
|
|
@@ -75,6 +73,17 @@ class LSTMGrade:
|
|
75
73
|
test_r2: float
|
76
74
|
|
77
75
|
|
76
|
+
@dataclass
|
77
|
+
class LSTMChartData:
|
78
|
+
ticker: str
|
79
|
+
labels: List[pandas.Timestamp]
|
80
|
+
prices: List[Dict[pandas.Timestamp, float]]
|
81
|
+
future_prices: List[Dict[pandas.Timestamp, float]]
|
82
|
+
grade: LSTMGrade
|
83
|
+
num: int
|
84
|
+
is_lstm_up: bool
|
85
|
+
|
86
|
+
|
78
87
|
class MyLSTM:
|
79
88
|
"""
|
80
89
|
주가 데이터를 기반으로 LSTM 모델을 생성, 학습 및 예측하는 클래스.
|
@@ -90,9 +99,10 @@ class MyLSTM:
|
|
90
99
|
future_days = 30
|
91
100
|
|
92
101
|
def __init__(self, ticker: str):
|
93
|
-
mylogger.info(f'set up ticker : {ticker}')
|
102
|
+
mylogger.info(f'MyLSTM set up ticker : {ticker}')
|
94
103
|
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
95
104
|
self._ticker = ticker
|
105
|
+
self.initialized = False
|
96
106
|
|
97
107
|
self.raw_data = pd.DataFrame()
|
98
108
|
self.lstm_data = LSTMData(
|
@@ -113,9 +123,10 @@ class MyLSTM:
|
|
113
123
|
|
114
124
|
@ticker.setter
|
115
125
|
def ticker(self, ticker: str):
|
116
|
-
mylogger.info(f'change ticker : {self.ticker} -> {ticker}')
|
126
|
+
mylogger.info(f'MyLstm change ticker : {self.ticker} -> {ticker}')
|
117
127
|
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
118
128
|
self._ticker = ticker
|
129
|
+
self.initialized = False
|
119
130
|
|
120
131
|
self.raw_data = pd.DataFrame()
|
121
132
|
self.lstm_data = LSTMData(
|
@@ -148,7 +159,7 @@ class MyLSTM:
|
|
148
159
|
|
149
160
|
# 4년 전 날짜 계산 (4년 = 365일 * 4)
|
150
161
|
four_years_ago = today - timedelta(days=365 * 4)
|
151
|
-
mylogger.
|
162
|
+
mylogger.debug(
|
152
163
|
f"Get raw data from yfinance - start: {four_years_ago.strftime('%Y-%m-%d')}, end: {today.strftime('%Y-%m-%d')}")
|
153
164
|
|
154
165
|
df = yf.download(
|
@@ -165,7 +176,7 @@ class MyLSTM:
|
|
165
176
|
lstm이 사용할 수 있도록 데이터 준비(정규화 및 8:2 훈련데이터 검증데이터 분리 및 차원변환)
|
166
177
|
:return:
|
167
178
|
"""
|
168
|
-
mylogger.
|
179
|
+
mylogger.debug("lstm이 사용할 수 있도록 데이터 준비(정규화 및 8:2 훈련데이터 검증데이터 분리 및 차원변환)")
|
169
180
|
# 필요한 열만 선택 (종가만 사용) - 2차웜 배열로 변환
|
170
181
|
data_2d = self.raw_data['Close'].values.reshape(-1, 1)
|
171
182
|
mylogger.debug(f"종가데이터 2차원베열값[:5] : {data_2d[:5]}")
|
@@ -178,7 +189,7 @@ class MyLSTM:
|
|
178
189
|
train_size = int(len(scaled_data_2d) * 0.8)
|
179
190
|
train_data_2d = scaled_data_2d[:train_size]
|
180
191
|
test_data_2d = scaled_data_2d[train_size:]
|
181
|
-
mylogger.
|
192
|
+
mylogger.debug(f'총 {len(data_2d)}개 데이터, train size : {train_size}')
|
182
193
|
|
183
194
|
# 학습 데이터에 대한 입력(X)과 정답(y)를 생성
|
184
195
|
def create_dataset(data, time_step=60):
|
@@ -228,10 +239,13 @@ class MyLSTM:
|
|
228
239
|
y_test_1d=y_test_1d,
|
229
240
|
)
|
230
241
|
|
242
|
+
mylogger.info("*** Initializing ***")
|
243
|
+
|
231
244
|
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
232
245
|
|
233
246
|
self.raw_data = get_raw_data()
|
234
247
|
self.lstm_data = preprocessing_for_lstm()
|
248
|
+
self.initialized = True
|
235
249
|
|
236
250
|
def ensemble_training(self, num) -> Tuple[list, LSTMGrade]:
|
237
251
|
"""
|
@@ -289,7 +303,7 @@ class MyLSTM:
|
|
289
303
|
"""
|
290
304
|
predictions_2d = model_in.predict(data)
|
291
305
|
predictions_scaled_2d = self.scaler.inverse_transform(predictions_2d) # 스케일링 복원
|
292
|
-
mylogger.
|
306
|
+
mylogger.debug(
|
293
307
|
f'predictions_scaled_2d : ndim - {predictions_scaled_2d.ndim} len - {len(predictions_scaled_2d)}') # numpy.ndarray 타입
|
294
308
|
mylogger.debug(f'predictions_scaled_2d[:5] :{predictions_scaled_2d[:5]}')
|
295
309
|
return predictions_scaled_2d
|
@@ -331,10 +345,10 @@ class MyLSTM:
|
|
331
345
|
test_r2 = r2_score(y_test_scaled_2d, mean_test_predictions_2d)
|
332
346
|
|
333
347
|
# 평가 결과 출력
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
348
|
+
mylogger.debug("Training Data:")
|
349
|
+
mylogger.debug(f"Train MSE: {train_mse}, Train MAE: {train_mae}, Train R²: {train_r2}")
|
350
|
+
mylogger.debug("\nTesting Data:")
|
351
|
+
mylogger.debug(f"Test MSE: {test_mse}, Test MAE: {test_mae}, Test R²: {test_r2}")
|
338
352
|
# mse, mae는 작을수록 좋으며 R^2은 0-1 사이값 1에 가까울수록 정확함
|
339
353
|
# 과적합에 대한 평가는 train 과 test를 비교하여 test가 너무 않좋으면 과적합 의심.
|
340
354
|
|
@@ -348,19 +362,21 @@ class MyLSTM:
|
|
348
362
|
test_r2=test_r2,
|
349
363
|
)
|
350
364
|
|
365
|
+
mylogger.info(f"*** ensemble training / num : {num} ***")
|
366
|
+
|
351
367
|
ensemble_train_predictions_2d = []
|
352
368
|
ensemble_test_predictions_2d = []
|
353
369
|
ensemble_future_predictions_2d = []
|
354
370
|
|
355
371
|
for i in range(num):
|
356
|
-
|
372
|
+
mylogger.info(f"Training model {i + 1}/{num}...")
|
357
373
|
model = model_training()
|
358
374
|
|
359
375
|
if len(model.layers) == 0:
|
360
376
|
mylogger.warning("이 모델은 빈 Sequential() 입니다.")
|
361
377
|
return [], grading([],[])
|
362
378
|
else:
|
363
|
-
mylogger.
|
379
|
+
mylogger.debug("레이어가 있는 모델입니다.")
|
364
380
|
|
365
381
|
# 훈련 데이터 예측
|
366
382
|
train_predictions_scaled_2d = prediction(model, self.lstm_data.X_train_3d)
|
@@ -390,11 +406,12 @@ class MyLSTM:
|
|
390
406
|
future_predictions_scaled_2d = self.scaler.inverse_transform(future_predictions_2d)
|
391
407
|
ensemble_future_predictions_2d.append(future_predictions_scaled_2d)
|
392
408
|
|
409
|
+
|
393
410
|
lstm_grade = grading(ensemble_train_predictions_2d, ensemble_test_predictions_2d)
|
394
411
|
|
395
412
|
return ensemble_future_predictions_2d, lstm_grade
|
396
413
|
|
397
|
-
def get_final_predictions(self, refresh: bool, num=5) -> Tuple[
|
414
|
+
def get_final_predictions(self, refresh: bool, num=5) -> Tuple[Dict[str, float], LSTMGrade]:
|
398
415
|
"""
|
399
416
|
LSTM 모델을 사용하여 미래 주가를 예측하고 평가 데이터를 반환합니다.
|
400
417
|
|
@@ -409,10 +426,10 @@ class MyLSTM:
|
|
409
426
|
- 캐시 데이터 검색 및 새 데이터 생성 과정 출력.
|
410
427
|
- 예측 값의 증가 추세를 분석하여 캐시에 저장.
|
411
428
|
"""
|
412
|
-
|
429
|
+
mylogger.info("**** Start get_final_predictions... ****")
|
413
430
|
redis_name = f'{self.ticker}_mylstm_predictions'
|
414
431
|
|
415
|
-
|
432
|
+
mylogger.info(
|
416
433
|
f"redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
417
434
|
|
418
435
|
def caching_is_lstm_up(future_data_in: dict):
|
@@ -424,9 +441,9 @@ class MyLSTM:
|
|
424
441
|
bool: 기울기가 양수이면 True, 아니면 False
|
425
442
|
"""
|
426
443
|
|
427
|
-
|
444
|
+
mylogger.debug("**** Caching is_lstm_up ... ****")
|
428
445
|
redis_name = f'{self.ticker}_is_lstm_up'
|
429
|
-
|
446
|
+
mylogger.debug(f"redisname: '{redis_name}' / expire_time : {expire_time / 3600}h")
|
430
447
|
|
431
448
|
is_up = tsa.common.is_up_by_OLS(future_data_in)
|
432
449
|
mylogger.debug(f"is_up: {is_up}")
|
@@ -456,7 +473,8 @@ class MyLSTM:
|
|
456
473
|
data[future_dates[i].strftime("%Y-%m-%d")] = final_future_predictions[i][0]
|
457
474
|
return data
|
458
475
|
|
459
|
-
self.
|
476
|
+
if not self.initialized:
|
477
|
+
self.initializing()
|
460
478
|
# 앙상블 트레이닝 시행
|
461
479
|
future_predictions_2d, lstm_grade = self.ensemble_training(num=num_in)
|
462
480
|
mylogger.debug(f'future_predictions_2d[:5] : {future_predictions_2d[:5]}')
|
@@ -477,159 +495,72 @@ class MyLSTM:
|
|
477
495
|
|
478
496
|
return future_data, lstm_grade
|
479
497
|
|
480
|
-
def
|
481
|
-
""
|
482
|
-
|
498
|
+
def generate_chart_data(self, refresh:bool, num=5) -> LSTMChartData:
|
499
|
+
mylogger.info("**** Start generate_lstm_chart_data... ****")
|
500
|
+
redis_name = f'{self.ticker}_lstm_chart_data'
|
483
501
|
|
484
|
-
|
485
|
-
refresh
|
486
|
-
to (str): 그래프 출력 형식 ('hrml', 'png', 'file'). 기본값은 'html'.
|
487
|
-
num (int): 예측 모델 수. 기본값은 5.
|
502
|
+
mylogger.info(
|
503
|
+
f"redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time / 3600}h")
|
488
504
|
|
489
|
-
|
490
|
-
|
491
|
-
|
505
|
+
def fetch_generate_lstm_chart_data(num_in) -> LSTMChartData:
|
506
|
+
def prepare_past_data(past_days) -> tuple:
|
507
|
+
mylogger.info("*** prepare past data ... ****")
|
508
|
+
# 데이터 준비
|
509
|
+
raw_data_copied = self.raw_data.reset_index()
|
510
|
+
data = raw_data_copied[['Date', 'Close']][-past_days:].reset_index(drop=True)
|
492
511
|
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
def prepare_past_data(past_days) -> tuple:
|
497
|
-
# 데이터 준비
|
498
|
-
raw_data_copied = self.raw_data.reset_index()
|
499
|
-
data = raw_data_copied[['Date', 'Close']][-past_days:].reset_index(drop=True)
|
500
|
-
|
501
|
-
# 'Date'와 'Close' 열 추출
|
502
|
-
past_dates = pd.to_datetime(data['Date'])
|
503
|
-
past_prices = data['Close']
|
504
|
-
|
505
|
-
# 'past_prices'가 Series인지 확인
|
506
|
-
if isinstance(past_prices, pd.DataFrame):
|
507
|
-
past_prices = past_prices.squeeze()
|
508
|
-
|
509
|
-
# 'Close' 열의 데이터 타입 변경
|
510
|
-
past_prices = past_prices.astype(float)
|
511
|
-
return past_dates, past_prices
|
512
|
-
|
513
|
-
def prepare_future_data(refresh_in, num_in) -> tuple:
|
514
|
-
future_data, lstm_grade = self.get_final_predictions(refresh=refresh_in, num=num_in)
|
515
|
-
|
516
|
-
# 예측 데이터 준비
|
517
|
-
future_dates = pd.to_datetime(list(future_data.keys()))
|
518
|
-
|
519
|
-
future_prices = pd.Series(future_data.values(), index=range(len(future_data.values()))).astype(float)
|
520
|
-
return future_dates, future_prices
|
521
|
-
|
522
|
-
self.initializing()
|
523
|
-
past_dates, past_prices = prepare_past_data(past_days=120)
|
524
|
-
future_dates, future_prices = prepare_future_data(refresh_in=refresh, num_in=num)
|
525
|
-
|
526
|
-
# 그래프 생성
|
527
|
-
fig = go.Figure()
|
528
|
-
|
529
|
-
# 실제 데이터 추가
|
530
|
-
fig.add_trace(go.Scatter(
|
531
|
-
x=past_dates,
|
532
|
-
y=past_prices,
|
533
|
-
mode='markers',
|
534
|
-
name='실제주가'
|
535
|
-
))
|
536
|
-
|
537
|
-
# 예측 데이터 추가
|
538
|
-
fig.add_trace(go.Scatter(
|
539
|
-
x=future_dates,
|
540
|
-
y=future_prices,
|
541
|
-
mode='lines+markers',
|
542
|
-
name='예측치(30일)'
|
543
|
-
))
|
544
|
-
|
545
|
-
# 레이아웃 업데이트
|
546
|
-
fig.update_layout(
|
547
|
-
xaxis_title='일자',
|
548
|
-
yaxis_title='주가(원)',
|
549
|
-
xaxis=dict(
|
550
|
-
tickformat='%Y/%m',
|
551
|
-
),
|
552
|
-
yaxis=dict(
|
553
|
-
tickformat=".0f",
|
554
|
-
),
|
555
|
-
showlegend=True,
|
556
|
-
)
|
512
|
+
# 'Date'와 'Close' 열 추출
|
513
|
+
past_dates = pd.to_datetime(data['Date'])
|
514
|
+
past_prices = data['Close']
|
557
515
|
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
mylogger.debug(f"future_prices({len(future_prices)}) - {future_prices}")
|
562
|
-
|
563
|
-
fig.update_layout(
|
564
|
-
# title=f'{self.code} {self.name} 주가 예측 그래프(prophet)',
|
565
|
-
xaxis_title='일자',
|
566
|
-
yaxis_title='주가(원)',
|
567
|
-
xaxis=dict(
|
568
|
-
tickformat='%Y/%m', # X축을 '연/월' 형식으로 표시
|
569
|
-
),
|
570
|
-
yaxis=dict(
|
571
|
-
tickformat=".0f", # 소수점 없이 원래 숫자 표시
|
572
|
-
),
|
573
|
-
showlegend=False,
|
574
|
-
)
|
516
|
+
# 'past_prices'가 Series인지 확인
|
517
|
+
if isinstance(past_prices, pd.DataFrame):
|
518
|
+
past_prices = past_prices.squeeze()
|
575
519
|
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
return graph_html
|
580
|
-
elif to == 'png':
|
581
|
-
# 그래프를 PNG 파일로 저장
|
582
|
-
fig.write_image(f"myLSTM_{self.ticker}.png")
|
583
|
-
return None
|
584
|
-
elif to == 'file':
|
585
|
-
# 그래프를 HTML로 저장
|
586
|
-
plot(fig, filename=f'myLSTM_{self.ticker}.html', auto_open=False)
|
587
|
-
return None
|
588
|
-
else:
|
589
|
-
Exception("to 인자가 맞지 않습니다.")
|
590
|
-
|
591
|
-
def visualization(self, refresh=True):
|
592
|
-
"""
|
593
|
-
실제 주가와 예측 주가를 시각화합니다.
|
520
|
+
# 'Close' 열의 데이터 타입 변경
|
521
|
+
past_prices = past_prices.astype(float)
|
522
|
+
return past_dates, past_prices
|
594
523
|
|
595
|
-
|
596
|
-
|
524
|
+
def prepare_future_data(refresh_in, num_in) -> tuple:
|
525
|
+
mylogger.info("*** prepare future data ... ****")
|
526
|
+
future_data, lstm_grade = self.get_final_predictions(refresh=refresh_in, num=num_in)
|
597
527
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
528
|
+
# 예측 데이터 준비
|
529
|
+
future_dates = pd.to_datetime(list(future_data.keys()))
|
530
|
+
|
531
|
+
future_prices = pd.Series(future_data.values(), index=range(len(future_data.values()))).astype(float)
|
532
|
+
return future_dates, future_prices, lstm_grade
|
533
|
+
|
534
|
+
if not self.initialized:
|
535
|
+
self.initializing()
|
536
|
+
past_dates, past_prices = prepare_past_data(past_days=120)
|
537
|
+
future_dates, future_prices, lstm_grade = prepare_future_data(refresh_in=refresh, num_in=num)
|
538
|
+
past_df = pd.DataFrame({"ds": past_dates, "y": past_prices})
|
539
|
+
future_df = pd.DataFrame({"ds": future_dates, "future_price": future_prices})
|
540
|
+
|
541
|
+
# 날짜(ds)를 기준으로 outer join
|
542
|
+
merged_df = pd.merge(past_df, future_df, on="ds", how="outer")
|
543
|
+
mylogger.debug(f"df_merged head: {merged_df.head()}")
|
544
|
+
mylogger.debug(f"df_merged tail: {merged_df.tail()}")
|
545
|
+
|
546
|
+
data = LSTMChartData(
|
547
|
+
ticker=self.ticker,
|
548
|
+
labels=merged_df["ds"].tolist(),
|
549
|
+
prices=[{"x": ds, "y": y} for ds, y in zip(merged_df["ds"], merged_df["y"]) if pd.notna(y)], # type: ignore
|
550
|
+
future_prices=[{"x": ds, "y": future_price} for ds, future_price in zip(merged_df["ds"], merged_df["future_price"])], # type: ignore
|
551
|
+
grade=lstm_grade,
|
552
|
+
num=num_in,
|
553
|
+
is_lstm_up=self.is_lstm_up(),
|
554
|
+
)
|
555
|
+
#import pprint
|
556
|
+
#pprint.pprint(data.prices[-10:], compact=True)
|
557
|
+
#pprint.pprint(data.future_prices[:10], compact=True)
|
558
|
+
#print(data.grade)
|
559
|
+
return data
|
560
|
+
|
561
|
+
lstm_chart_data = myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_lstm_chart_data,
|
562
|
+
num, timer=expire_time)
|
563
|
+
return lstm_chart_data
|
633
564
|
|
634
565
|
def is_lstm_up(self) -> bool:
|
635
566
|
"""
|
@@ -640,6 +571,16 @@ class MyLSTM:
|
|
640
571
|
"""
|
641
572
|
return myredis.Base.get_value(f'{self.ticker}_is_lstm_up')
|
642
573
|
|
574
|
+
@staticmethod
|
575
|
+
def caching_chart_data(tickers:list, num:int):
|
576
|
+
mylogger.info(f"*** caching_chart_data ***")
|
577
|
+
mylstm = MyLSTM('005930.KS')
|
578
|
+
for i, ticker in enumerate(tickers):
|
579
|
+
mylstm.ticker = ticker
|
580
|
+
mylogger.info(f"{i + 1}. {ticker}")
|
581
|
+
chart_data = mylstm.generate_chart_data(refresh=True, num=num)
|
582
|
+
mylogger.debug(chart_data)
|
583
|
+
|
643
584
|
|
644
585
|
class CorpLSTM(MyLSTM):
|
645
586
|
"""
|
@@ -666,6 +607,26 @@ class CorpLSTM(MyLSTM):
|
|
666
607
|
self.name = myredis.Corps(code, 'c101').get_name()
|
667
608
|
self.ticker = self.code + '.KS'
|
668
609
|
|
610
|
+
@staticmethod
|
611
|
+
def ticker_to_code(ticker: str):
|
612
|
+
return ticker[:-3]
|
613
|
+
|
614
|
+
@staticmethod
|
615
|
+
def code_to_ticker(code: str):
|
616
|
+
return code + '.KS'
|
617
|
+
|
618
|
+
@staticmethod
|
619
|
+
def caching_chart_data_topn(top=40, num=5):
|
620
|
+
mylogger.info(f"*** caching_chart_data_topn : {top} ***")
|
621
|
+
ranking_topn = tsa.CorpProphet.ranking(refresh=False, top=top)
|
622
|
+
mylogger.debug(ranking_topn)
|
623
|
+
MyLSTM.caching_chart_data([CorpLSTM.code_to_ticker(code) for code in ranking_topn.keys() ], num=num)
|
624
|
+
|
625
|
+
@staticmethod
|
626
|
+
def caching_chart_data_bulk(codes: list, num=5):
|
627
|
+
mylogger.info(f"*** caching_chart_data_bulk : {len(codes)} items ***")
|
628
|
+
MyLSTM.caching_chart_data([CorpLSTM.code_to_ticker(code) for code in codes], num=num)
|
629
|
+
|
669
630
|
|
670
631
|
class MILSTM(MyLSTM):
|
671
632
|
"""
|
@@ -689,3 +650,11 @@ class MILSTM(MyLSTM):
|
|
689
650
|
self._mi_type = mi_type
|
690
651
|
self.ticker = getattr(MIs, mi_type)
|
691
652
|
|
653
|
+
@staticmethod
|
654
|
+
def caching_chart_data_mi_all(num=5):
|
655
|
+
mylogger.info(f"*** caching_chart_data_mi_all ***")
|
656
|
+
mylogger.debug(f"mi_type : {MIs._fields}")
|
657
|
+
MyLSTM.caching_chart_data([getattr(MIs, mi_type) for mi_type in MIs._fields], num=num)
|
658
|
+
|
659
|
+
|
660
|
+
|