analyser_hj3415 2.8.2__py2.py3-none-any.whl → 2.9.0__py2.py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- analyser_hj3415/cli.py +25 -8
- analyser_hj3415/eval.py +71 -71
- analyser_hj3415/tsa.py +426 -18
- {analyser_hj3415-2.8.2.dist-info → analyser_hj3415-2.9.0.dist-info}/METADATA +1 -1
- {analyser_hj3415-2.8.2.dist-info → analyser_hj3415-2.9.0.dist-info}/RECORD +8 -9
- analyser_hj3415/workroom/lstm.py +0 -115
- {analyser_hj3415-2.8.2.dist-info → analyser_hj3415-2.9.0.dist-info}/LICENSE +0 -0
- {analyser_hj3415-2.8.2.dist-info → analyser_hj3415-2.9.0.dist-info}/WHEEL +0 -0
- {analyser_hj3415-2.8.2.dist-info → analyser_hj3415-2.9.0.dist-info}/entry_points.txt +0 -0
analyser_hj3415/cli.py
CHANGED
@@ -41,15 +41,22 @@ def analyser_manager():
|
|
41
41
|
parser = argparse.ArgumentParser(description="Analyser Commands")
|
42
42
|
type_subparsers = parser.add_subparsers(dest='type', help='분석 타입')
|
43
43
|
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
#
|
48
|
-
ranking_parser =
|
49
|
-
ranking_parser.add_argument('-e', '--expect_earn', type=float, help='기대수익률 (실수 값 입력)')
|
44
|
+
# prophet 명령어 서브파서
|
45
|
+
prophet_parser = type_subparsers.add_parser('prophet', help='MyProphet 타입')
|
46
|
+
prophet_subparser = prophet_parser.add_subparsers(dest='command', help='prophet 관련된 명령')
|
47
|
+
# ranking 파서
|
48
|
+
ranking_parser = prophet_subparser.add_parser('ranking', help='prophet 랭킹 책정 및 레디스 저장')
|
50
49
|
ranking_parser.add_argument('-r', '--refresh', action='store_true', help='래디스 캐시를 사용하지 않고 강제로 재계산 할지')
|
51
50
|
ranking_parser.add_argument('-n', '--noti', action='store_true', help='작업 완료 후 메시지 전송 여부')
|
52
51
|
|
52
|
+
# lstm 명령어 서브파서
|
53
|
+
lstm_parser = type_subparsers.add_parser('lstm', help='MyLSTM 타입')
|
54
|
+
lstm_subparser = lstm_parser.add_subparsers(dest='command', help='lstm 관련된 명령')
|
55
|
+
# caching 파서
|
56
|
+
caching_parser = lstm_subparser.add_parser('caching', help='lstm 랭킹 책정 및 레디스 저장')
|
57
|
+
caching_parser.add_argument('-r', '--refresh', action='store_true', help='래디스 캐시를 사용하지 않고 강제로 재계산 할지')
|
58
|
+
caching_parser.add_argument('-n', '--noti', action='store_true', help='작업 완료 후 메시지 전송 여부')
|
59
|
+
|
53
60
|
# red 명령어 서브파서
|
54
61
|
red_parser = type_subparsers.add_parser('red', help='red 타입')
|
55
62
|
red_subparser = red_parser.add_subparsers(dest='command', help='red 관련된 명령')
|
@@ -110,7 +117,7 @@ def analyser_manager():
|
|
110
117
|
|
111
118
|
args = parser.parse_args()
|
112
119
|
|
113
|
-
from analyser_hj3415 import eval
|
120
|
+
from analyser_hj3415 import eval, tsa
|
114
121
|
|
115
122
|
if args.type == 'red':
|
116
123
|
if args.command == 'get':
|
@@ -204,7 +211,17 @@ def analyser_manager():
|
|
204
211
|
pprint.pprint(growth.get(args.refresh))
|
205
212
|
if args.noti:
|
206
213
|
noti.telegram_to('manager', f"오늘의 Growth({args.code})를 레디스 캐시에 저장했습니다.(유효 12시간)")
|
207
|
-
|
214
|
+
elif args.type == 'prophet':
|
215
|
+
if args.command == 'ranking':
|
216
|
+
result = tsa.MyProphet.ranking(refresh=args.refresh)
|
217
|
+
print(result)
|
218
|
+
if args.noti:
|
219
|
+
noti.telegram_to('manager', "오늘의 prophet ranking을 레디스캐시에 저장했습니다.(유효 24시간)")
|
220
|
+
elif args.type == 'lstm':
|
221
|
+
if args.command == 'caching':
|
222
|
+
result = tsa.MyLSTM('005930').caching_based_on_prophet_ranking(refresh=args.refresh)
|
223
|
+
if args.noti:
|
224
|
+
noti.telegram_to('manager', "오늘의 lstm caching을 레디스캐시에 저장했습니다.(유효 24시간)")
|
208
225
|
elif args.type == 'setting':
|
209
226
|
if args.command == 'set':
|
210
227
|
settings_manager.set_value(args.title, args.value)
|
analyser_hj3415/eval.py
CHANGED
@@ -8,7 +8,7 @@ from analyser_hj3415.cli import AnalyserSettingsManager
|
|
8
8
|
from collections import OrderedDict
|
9
9
|
import logging
|
10
10
|
|
11
|
-
|
11
|
+
eval_logger = helpers.setup_logger('eval_logger', logging.WARNING)
|
12
12
|
|
13
13
|
expire_time = 3600 * 12
|
14
14
|
|
@@ -48,30 +48,30 @@ class Tools:
|
|
48
48
|
"""
|
49
49
|
name = myredis.Corps(c103.code, 'c101').get_name(refresh=refresh)
|
50
50
|
|
51
|
-
|
51
|
+
eval_logger.info(f'{c103.code} / {name} Tools : 당기순이익 계산.. refresh : {refresh}')
|
52
52
|
c103.page = 'c103재무상태표q'
|
53
53
|
|
54
54
|
d1, 지배당기순이익 = c103.latest_value_pop2('*(지배)당기순이익', refresh)
|
55
|
-
|
55
|
+
eval_logger.debug(f"*(지배)당기순이익: {지배당기순이익}")
|
56
56
|
|
57
57
|
if math.isnan(지배당기순이익):
|
58
|
-
|
58
|
+
eval_logger.warning(f"{c103.code} / {name} - (지배)당기순이익이 없는 종목. 수동으로 계산합니다.")
|
59
59
|
c103.page = 'c103손익계산서q'
|
60
60
|
d2, 최근4분기당기순이익 = c103.sum_recent_4q('당기순이익', refresh)
|
61
|
-
|
61
|
+
eval_logger.debug(f"{c103.code} / {name} - 최근4분기당기순이익 : {최근4분기당기순이익}")
|
62
62
|
c103.page = 'c103재무상태표y'
|
63
63
|
d3, 비지배당기순이익 = c103.latest_value_pop2('*(비지배)당기순이익', refresh)
|
64
|
-
|
64
|
+
eval_logger.debug(f"{c103.code} / {name} - 비지배당기순이익y : {비지배당기순이익}")
|
65
65
|
# 가변리스트 언패킹으로 하나의 날짜만 사용하고 나머지는 버린다.
|
66
66
|
# 여기서 *_는 “나머지 값을 다 무시하겠다”는 의미
|
67
|
-
|
67
|
+
eval_logger.debug(f"d2:{d2}, d3: {d3}")
|
68
68
|
try:
|
69
69
|
date, *_ = Tools.date_set(d2, d3)
|
70
70
|
except ValueError:
|
71
71
|
# 날짜 데이터가 없는경우
|
72
72
|
date = ''
|
73
73
|
계산된지배당기순이익= round(최근4분기당기순이익 - utils.nan_to_zero(비지배당기순이익), 1)
|
74
|
-
|
74
|
+
eval_logger.debug(f"{c103.code} / {name} - 계산된 지배당기순이익 : {계산된지배당기순이익}")
|
75
75
|
return date, 계산된지배당기순이익
|
76
76
|
else:
|
77
77
|
return d1, 지배당기순이익
|
@@ -85,20 +85,20 @@ class Tools:
|
|
85
85
|
"""
|
86
86
|
name = myredis.Corps(c103.code, 'c101').get_name(refresh=refresh)
|
87
87
|
|
88
|
-
|
88
|
+
eval_logger.info(f'{c103.code} / {name} Tools : 유동자산계산... refresh : {refresh}')
|
89
89
|
c103.page = 'c103재무상태표q'
|
90
90
|
|
91
91
|
d, 유동자산 = c103.sum_recent_4q('유동자산', refresh)
|
92
92
|
if math.isnan(유동자산):
|
93
|
-
|
93
|
+
eval_logger.warning(f"{c103.code} / {name} - 유동자산이 없는 종목. 수동으로 계산합니다(금융관련업종일 가능성있음).")
|
94
94
|
d1, v1 = c103.latest_value_pop2('현금및예치금', refresh)
|
95
95
|
d2, v2 = c103.latest_value_pop2('단기매매금융자산', refresh)
|
96
96
|
d3, v3 = c103.latest_value_pop2('매도가능금융자산', refresh)
|
97
97
|
d4, v4 = c103.latest_value_pop2('만기보유금융자산', refresh)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
98
|
+
eval_logger.debug(f'{c103.code} / {name} 현금및예치금 : {d1}, {v1}')
|
99
|
+
eval_logger.debug(f'{c103.code} / {name} 단기매매금융자산 : {d2}, {v2}')
|
100
|
+
eval_logger.debug(f'{c103.code} / {name} 매도가능금융자산 : {d3}, {v3}')
|
101
|
+
eval_logger.debug(f'{c103.code} / {name} 만기보유금융자산 : {d4}, {v4}')
|
102
102
|
|
103
103
|
try:
|
104
104
|
date, *_ = Tools.date_set(d1, d2, d3, d4)
|
@@ -107,7 +107,7 @@ class Tools:
|
|
107
107
|
date = ''
|
108
108
|
계산된유동자산value = round(utils.nan_to_zero(v1) + utils.nan_to_zero(v2) + utils.nan_to_zero(v3) + utils.nan_to_zero(v4),1)
|
109
109
|
|
110
|
-
|
110
|
+
eval_logger.info(f"{c103.code} / {name} - 계산된 유동자산 : {계산된유동자산value}")
|
111
111
|
return date, 계산된유동자산value
|
112
112
|
else:
|
113
113
|
return d, 유동자산
|
@@ -121,20 +121,20 @@ class Tools:
|
|
121
121
|
"""
|
122
122
|
name = myredis.Corps(c103.code, 'c101').get_name(refresh=refresh)
|
123
123
|
|
124
|
-
|
124
|
+
eval_logger.info(f'{c103.code} / {name} Tools : 유동부채계산... refresh : {refresh}')
|
125
125
|
c103.page = 'c103재무상태표q'
|
126
126
|
|
127
127
|
d, 유동부채 = c103.sum_recent_4q('유동부채', refresh)
|
128
128
|
if math.isnan(유동부채):
|
129
|
-
|
129
|
+
eval_logger.warning(f"{c103.code} / {name} - 유동부채가 없는 종목. 수동으로 계산합니다.")
|
130
130
|
d1, v1 = c103.latest_value_pop2('당기손익인식(지정)금융부채', refresh)
|
131
131
|
d2, v2 = c103.latest_value_pop2('당기손익-공정가치측정금융부채', refresh)
|
132
132
|
d3, v3 = c103.latest_value_pop2('매도파생결합증권', refresh)
|
133
133
|
d4, v4 = c103.latest_value_pop2('단기매매금융부채', refresh)
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
134
|
+
eval_logger.debug(f'{c103.code} / {name} 당기손익인식(지정)금융부채 : {d1}, {v1}')
|
135
|
+
eval_logger.debug(f'{c103.code} / {name} 당기손익-공정가치측정금융부채 : {d2}, {v2}')
|
136
|
+
eval_logger.debug(f'{c103.code} / {name} 매도파생결합증권 : {d3}, {v3}')
|
137
|
+
eval_logger.debug(f'{c103.code} / {name} 단기매매금융부채 : {d4}, {v4}')
|
138
138
|
|
139
139
|
try:
|
140
140
|
date, *_ = Tools.date_set(d1, d2, d3, d4)
|
@@ -143,7 +143,7 @@ class Tools:
|
|
143
143
|
date = ''
|
144
144
|
계산된유동부채value = round(utils.nan_to_zero(v1) + utils.nan_to_zero(v2) + utils.nan_to_zero(v3) + utils.nan_to_zero(v4), 1)
|
145
145
|
|
146
|
-
|
146
|
+
eval_logger.info(f"{c103.code} / {name} - 계산된 유동부채 : {계산된유동부채value}")
|
147
147
|
return date, 계산된유동부채value
|
148
148
|
else:
|
149
149
|
return d, 유동부채
|
@@ -192,7 +192,7 @@ class Red:
|
|
192
192
|
|
193
193
|
def __init__(self, code: str):
|
194
194
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
195
|
-
|
195
|
+
eval_logger.debug(f"Red : 초기화 ({code})")
|
196
196
|
self.c101 = myredis.C101(code)
|
197
197
|
self.c103 = myredis.C103(code, 'c103재무상태표q')
|
198
198
|
|
@@ -209,7 +209,7 @@ class Red:
|
|
209
209
|
@code.setter
|
210
210
|
def code(self, code: str):
|
211
211
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
212
|
-
|
212
|
+
eval_logger.debug(f"Red : 종목코드 변경({self.code} -> {code})")
|
213
213
|
self.c101.code = code
|
214
214
|
self.c103.code = code
|
215
215
|
|
@@ -222,21 +222,21 @@ class Red:
|
|
222
222
|
일반적인 경우로 비유동부채를 찾아서 반환한다.\n
|
223
223
|
금융기관의 경우는 간접적으로 계산한다.\n
|
224
224
|
"""
|
225
|
-
|
225
|
+
eval_logger.info(f'In the calc비유동부채... refresh : {refresh}')
|
226
226
|
self.c103.page = 'c103재무상태표q'
|
227
227
|
|
228
228
|
d, 비유동부채 = self.c103.sum_recent_4q('비유동부채', refresh)
|
229
229
|
if math.isnan(비유동부채):
|
230
|
-
|
230
|
+
eval_logger.warning(f"{self} - 비유동부채가 없는 종목. 수동으로 계산합니다.")
|
231
231
|
# 보험관련업종은 예수부채가 없는대신 보험계약부채가 있다...
|
232
232
|
d1, v1 = self.c103.latest_value_pop2('예수부채', refresh)
|
233
233
|
d2, v2 = self.c103.latest_value_pop2('보험계약부채(책임준비금)', refresh)
|
234
234
|
d3, v3 = self.c103.latest_value_pop2('차입부채', refresh)
|
235
235
|
d4, v4 = self.c103.latest_value_pop2('기타부채', refresh)
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
236
|
+
eval_logger.debug(f'예수부채 : {d1}, {v1}')
|
237
|
+
eval_logger.debug(f'보험계약부채(책임준비금) : {d2}, {v2}')
|
238
|
+
eval_logger.debug(f'차입부채 : {d3}, {v3}')
|
239
|
+
eval_logger.debug(f'기타부채 : {d4}, {v4}')
|
240
240
|
|
241
241
|
try:
|
242
242
|
date, *_ = Tools.date_set(d1, d2, d3, d4)
|
@@ -244,7 +244,7 @@ class Red:
|
|
244
244
|
# 날짜 데이터가 없는경우
|
245
245
|
date = ''
|
246
246
|
계산된비유동부채value = round(utils.nan_to_zero(v1) + utils.nan_to_zero(v2) + utils.nan_to_zero(v3) + utils.nan_to_zero(v4),1)
|
247
|
-
|
247
|
+
eval_logger.info(f"{self} - 계산된 비유동부채 : {계산된비유동부채value}")
|
248
248
|
return date, 계산된비유동부채value
|
249
249
|
else:
|
250
250
|
return d, 비유동부채
|
@@ -266,13 +266,13 @@ class Red:
|
|
266
266
|
else:
|
267
267
|
score = utils.to_int(math.log10(deviation + 1) * 33) # desmos그래프상 33이 제일 적당한듯(최대100점에 가깝게)
|
268
268
|
|
269
|
-
|
269
|
+
eval_logger.debug(f"최근주가 : {recent_price} red가격 : {red_price} 괴리율 : {utils.to_int(deviation)} score : {score}")
|
270
270
|
|
271
271
|
return score
|
272
272
|
|
273
273
|
def _generate_data(self, refresh: bool) -> RedData:
|
274
274
|
d1, 지배주주당기순이익 = Tools.calc당기순이익(self.c103, refresh)
|
275
|
-
|
275
|
+
eval_logger.debug(f"{self} 지배주주당기순이익: {지배주주당기순이익}")
|
276
276
|
d2, 유동자산 = Tools.calc유동자산(self.c103, refresh)
|
277
277
|
d3, 유동부채 = Tools.calc유동부채(self.c103, refresh)
|
278
278
|
d4, 부채평가 = self._calc비유동부채(refresh)
|
@@ -331,7 +331,7 @@ class Red:
|
|
331
331
|
:return:
|
332
332
|
"""
|
333
333
|
redis_name = f"{self.code}_red"
|
334
|
-
|
334
|
+
eval_logger.info(f"{self} RedData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
|
335
335
|
expire_time = 3600 * 12
|
336
336
|
if verbose:
|
337
337
|
print(f"{self} redisname: '{redis_name}' / expect_earn: {Red.expect_earn} / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
@@ -355,11 +355,11 @@ class Red:
|
|
355
355
|
# expect_earn 및 refresh 설정
|
356
356
|
if expect_earn is None:
|
357
357
|
expect_earn = cls.expect_earn
|
358
|
-
|
358
|
+
eval_logger.info(f"기대수익률을 {expect_earn}으로 설정합니다.")
|
359
359
|
previous_expect_earn = float(AnalyserSettingsManager().get_value('RED_RANKING_EXPECT_EARN'))
|
360
|
-
|
360
|
+
eval_logger.debug(f"previous red ranking expect earn : {previous_expect_earn}")
|
361
361
|
if previous_expect_earn != expect_earn:
|
362
|
-
|
362
|
+
eval_logger.warning(f"expect earn : {expect_earn} / RED_RANKING_EXPECT_EARN : {previous_expect_earn} 두 값이 달라 refresh = True")
|
363
363
|
refresh = True
|
364
364
|
|
365
365
|
redis_name = 'red_ranking'
|
@@ -422,7 +422,7 @@ class MilData:
|
|
422
422
|
class Mil:
|
423
423
|
def __init__(self, code: str):
|
424
424
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
425
|
-
|
425
|
+
eval_logger.debug(f"Mil : 종목코드 ({code})")
|
426
426
|
|
427
427
|
self.c101 = myredis.C101(code)
|
428
428
|
self.c103 = myredis.C103(code, 'c103현금흐름표q')
|
@@ -442,7 +442,7 @@ class Mil:
|
|
442
442
|
@code.setter
|
443
443
|
def code(self, code: str):
|
444
444
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
445
|
-
|
445
|
+
eval_logger.debug(f"Mil : 종목코드 변경({self.code} -> {code})")
|
446
446
|
|
447
447
|
self.c101.code = code
|
448
448
|
self.c103.code = code
|
@@ -459,7 +459,7 @@ class Mil:
|
|
459
459
|
"""
|
460
460
|
c101r = self.c101.get_recent(refresh)
|
461
461
|
시가총액 = int(utils.to_int(c101r.get('시가총액', math.nan)) / 100000000)
|
462
|
-
|
462
|
+
eval_logger.debug(f"시가총액: {시가총액}억원")
|
463
463
|
return 시가총액
|
464
464
|
|
465
465
|
def _calc주주수익률(self, 시가총액_억: float, refresh: bool) -> Tuple[str, float, float]:
|
@@ -469,7 +469,7 @@ class Mil:
|
|
469
469
|
주주수익률 = round((재무활동현금흐름 / 시가총액_억 * -100), 2)
|
470
470
|
except ZeroDivisionError:
|
471
471
|
주주수익률 = math.nan
|
472
|
-
|
472
|
+
eval_logger.warning(f'{self} 주주수익률: {주주수익률} 재무활동현금흐름: {재무활동현금흐름}')
|
473
473
|
return d, 주주수익률, 재무활동현금흐름
|
474
474
|
|
475
475
|
def _calc이익지표(self, 시가총액_억: float, refresh: bool) -> Tuple[str, float, float, float]:
|
@@ -480,7 +480,7 @@ class Mil:
|
|
480
480
|
이익지표 = round(((지배주주당기순이익 - 영업활동현금흐름) / 시가총액_억) * 100, 2)
|
481
481
|
except ZeroDivisionError:
|
482
482
|
이익지표 = math.nan
|
483
|
-
|
483
|
+
eval_logger.warning(f'{self} 이익지표: {이익지표} 영업활동현금흐름: {영업활동현금흐름} 지배주주당기순이익: {지배주주당기순이익}')
|
484
484
|
try:
|
485
485
|
date, *_ = Tools.date_set(d1, d2)
|
486
486
|
except ValueError:
|
@@ -521,15 +521,15 @@ class Mil:
|
|
521
521
|
self.c103.page = 'c103재무상태표y'
|
522
522
|
_, capex = self.c103.find('*CAPEX', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
|
523
523
|
|
524
|
-
|
525
|
-
|
524
|
+
eval_logger.debug(f'영업활동현금흐름 {영업활동현금흐름_dict}')
|
525
|
+
eval_logger.debug(f'CAPEX {capex}')
|
526
526
|
|
527
527
|
if len(영업활동현금흐름_dict) == 0:
|
528
528
|
return {}
|
529
529
|
|
530
530
|
if len(capex) == 0:
|
531
531
|
# CAPEX 가 없는 업종은 영업활동현금흐름을 그대로 사용한다.
|
532
|
-
|
532
|
+
eval_logger.warning(f"{self} - CAPEX가 없는 업종으로 영업현금흐름을 그대로 사용합니다..")
|
533
533
|
return 영업활동현금흐름_dict
|
534
534
|
|
535
535
|
# 영업 활동으로 인한 현금 흐름에서 CAPEX 를 각 연도별로 빼주어 fcf 를 구하고 리턴값으로 fcf 딕셔너리를 반환한다.
|
@@ -543,7 +543,7 @@ class Mil:
|
|
543
543
|
if 영업활동현금흐름date == CAPEXdate:
|
544
544
|
fcf_dict[영업활동현금흐름date] = round(영업활동현금흐름value - CAPEXvalue, 2)
|
545
545
|
|
546
|
-
|
546
|
+
eval_logger.debug(f'fcf_dict {fcf_dict}')
|
547
547
|
# 연도순으로 정렬해서 딕셔너리로 반환한다.
|
548
548
|
return dict(sorted(fcf_dict.items(), reverse=False))
|
549
549
|
|
@@ -556,7 +556,7 @@ class Mil:
|
|
556
556
|
https://www.investopedia.com/terms/p/pricetofreecashflow.asp
|
557
557
|
"""
|
558
558
|
if math.isnan(시가총액_억):
|
559
|
-
|
559
|
+
eval_logger.warning(f"{self} - 시가총액이 nan으로 pFCF를 계산할수 없습니다.")
|
560
560
|
return {}
|
561
561
|
|
562
562
|
# pfcf 계산
|
@@ -569,7 +569,7 @@ class Mil:
|
|
569
569
|
|
570
570
|
pfcf_dict = mymongo.C1034.del_unnamed_key(pfcf_dict)
|
571
571
|
|
572
|
-
|
572
|
+
eval_logger.debug(f'pfcf_dict : {pfcf_dict}')
|
573
573
|
return pfcf_dict
|
574
574
|
|
575
575
|
def _calc가치지표(self, 시가총액_억: float, refresh: bool) -> tuple:
|
@@ -585,15 +585,15 @@ class Mil:
|
|
585
585
|
return [0,]
|
586
586
|
|
587
587
|
def _generate_data(self, refresh: bool) -> MilData:
|
588
|
-
|
588
|
+
eval_logger.info(f"In generate_data..refresh : {refresh}")
|
589
589
|
시가총액_억 = self.get_marketcap억(refresh)
|
590
|
-
|
590
|
+
eval_logger.info(f"{self} 시가총액(억) : {시가총액_억}")
|
591
591
|
|
592
592
|
d1, 주주수익률, 재무활동현금흐름 = self._calc주주수익률(시가총액_억, refresh)
|
593
|
-
|
593
|
+
eval_logger.info(f"{self} 주주수익률 : {주주수익률}, {d1}")
|
594
594
|
|
595
595
|
d2, 이익지표, 영업활동현금흐름, 지배주주당기순이익 = self._calc이익지표(시가총액_억, refresh)
|
596
|
-
|
596
|
+
eval_logger.info(f"{self} 이익지표 : {이익지표}, {d2}")
|
597
597
|
|
598
598
|
d3, roic_r, roic_dict, roe_r, roe106, roa_r = self._calc투자수익률(refresh)
|
599
599
|
d4, fcf_dict, pfcf_dict, pcr_dict = self._calc가치지표(시가총액_억, refresh)
|
@@ -640,7 +640,7 @@ class Mil:
|
|
640
640
|
:return:
|
641
641
|
"""
|
642
642
|
redis_name = f"{self.code}_mil"
|
643
|
-
|
643
|
+
eval_logger.info(f"{self} MilData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
|
644
644
|
if verbose:
|
645
645
|
print(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
646
646
|
|
@@ -677,7 +677,7 @@ class BlueData:
|
|
677
677
|
class Blue:
|
678
678
|
def __init__(self, code: str):
|
679
679
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
680
|
-
|
680
|
+
eval_logger.debug(f"Blue : 종목코드 ({code})")
|
681
681
|
|
682
682
|
self.c101 = myredis.C101(code)
|
683
683
|
self.c103 = myredis.C103(code, 'c103재무상태표q')
|
@@ -696,7 +696,7 @@ class Blue:
|
|
696
696
|
@code.setter
|
697
697
|
def code(self, code: str):
|
698
698
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
699
|
-
|
699
|
+
eval_logger.debug(f"Blue : 종목코드 변경({self.code} -> {code})")
|
700
700
|
|
701
701
|
self.c101.code = code
|
702
702
|
self.c103.code = code
|
@@ -711,11 +711,11 @@ class Blue:
|
|
711
711
|
c104q에서 최근유동비율 찾아보고 유효하지 않거나 \n
|
712
712
|
100이하인 경우에는수동으로 계산해서 다시 한번 평가해 본다.\n
|
713
713
|
"""
|
714
|
-
|
714
|
+
eval_logger.info(f'In the calc유동비율... refresh : {refresh}')
|
715
715
|
self.c104.page = 'c104q'
|
716
716
|
|
717
717
|
유동비율date, 유동비율value = self.c104.latest_value('유동비율', pop_count=pop_count)
|
718
|
-
|
718
|
+
eval_logger.info(f'{self} 유동비율 : {유동비율value}/({유동비율date})')
|
719
719
|
|
720
720
|
if math.isnan(유동비율value) or 유동비율value < 100:
|
721
721
|
유동자산date, 유동자산value = Tools.calc유동자산(self.c103, refresh)
|
@@ -723,23 +723,23 @@ class Blue:
|
|
723
723
|
|
724
724
|
self.c103.page = 'c103현금흐름표q'
|
725
725
|
추정영업현금흐름date, 추정영업현금흐름value = self.c103.sum_recent_4q('영업활동으로인한현금흐름', refresh)
|
726
|
-
|
726
|
+
eval_logger.debug(f'{self} 계산전 유동비율 : {유동비율value} / ({유동비율date})')
|
727
727
|
|
728
728
|
계산된유동비율 = 0
|
729
729
|
try:
|
730
730
|
계산된유동비율 = round(((유동자산value + 추정영업현금흐름value) / 유동부채value) * 100, 2)
|
731
731
|
except ZeroDivisionError:
|
732
|
-
|
732
|
+
eval_logger.info(f'유동자산: {유동자산value} + 추정영업현금흐름: {추정영업현금흐름value} / 유동부채: {유동부채value}')
|
733
733
|
계산된유동비율 = float('inf')
|
734
734
|
finally:
|
735
|
-
|
735
|
+
eval_logger.debug(f'{self} 계산된 유동비율 : {계산된유동비율}')
|
736
736
|
|
737
737
|
try:
|
738
738
|
date, *_ = Tools.date_set(유동자산date, 유동부채date, 추정영업현금흐름date)
|
739
739
|
except ValueError:
|
740
740
|
# 날짜 데이터가 없는경우
|
741
741
|
date = ''
|
742
|
-
|
742
|
+
eval_logger.warning(f'{self} 유동비율 이상(100 이하 또는 nan) : {유동비율value} -> 재계산 : {계산된유동비율}')
|
743
743
|
return date, 계산된유동비율
|
744
744
|
else:
|
745
745
|
return 유동비율date, 유동비율value
|
@@ -749,7 +749,7 @@ class Blue:
|
|
749
749
|
|
750
750
|
def _generate_data(self, refresh: bool) -> BlueData:
|
751
751
|
d1, 유동비율 = self._calc유동비율(pop_count=3, refresh=refresh)
|
752
|
-
|
752
|
+
eval_logger.info(f'유동비율 {유동비율} / [{d1}]')
|
753
753
|
|
754
754
|
재고자산회전율_c106 = myredis.C106.make_like_c106(self.code, 'c104q', '재고자산회전율', refresh)
|
755
755
|
|
@@ -766,16 +766,16 @@ class Blue:
|
|
766
766
|
d9, 순부채비율_r = self.c104.latest_value_pop2('순부채비율', refresh)
|
767
767
|
|
768
768
|
if len(이자보상배율_dict) == 0:
|
769
|
-
|
769
|
+
eval_logger.warning(f'empty dict - 이자보상배율 : {이자보상배율_r} / {이자보상배율_dict}')
|
770
770
|
|
771
771
|
if len(순운전자본회전율_dict) == 0:
|
772
|
-
|
772
|
+
eval_logger.warning(f'empty dict - 순운전자본회전율 : {순운전자본회전율_r} / {순운전자본회전율_dict}')
|
773
773
|
|
774
774
|
if len(재고자산회전율_dict) == 0:
|
775
|
-
|
775
|
+
eval_logger.warning(f'empty dict - 재고자산회전율 : {재고자산회전율_r} / {재고자산회전율_dict}')
|
776
776
|
|
777
777
|
if len(순부채비율_dict) == 0:
|
778
|
-
|
778
|
+
eval_logger.warning(f'empty dict - 순부채비율 : {순부채비율_r} / {순부채비율_dict}')
|
779
779
|
|
780
780
|
score = self._score()
|
781
781
|
|
@@ -813,7 +813,7 @@ class Blue:
|
|
813
813
|
:return:
|
814
814
|
"""
|
815
815
|
redis_name = f"{self.code}_blue"
|
816
|
-
|
816
|
+
eval_logger.info(f"{self} BlueData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
|
817
817
|
if verbose:
|
818
818
|
print(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
819
819
|
|
@@ -841,7 +841,7 @@ class GrowthData:
|
|
841
841
|
class Growth:
|
842
842
|
def __init__(self, code: str):
|
843
843
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
844
|
-
|
844
|
+
eval_logger.debug(f"Growth : 종목코드 ({code})")
|
845
845
|
|
846
846
|
self.c101 = myredis.C101(code)
|
847
847
|
self.c104 = myredis.C104(code, 'c104q')
|
@@ -860,7 +860,7 @@ class Growth:
|
|
860
860
|
@code.setter
|
861
861
|
def code(self, code: str):
|
862
862
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
863
|
-
|
863
|
+
eval_logger.debug(f"Growth : 종목코드 변경({self.code} -> {code})")
|
864
864
|
|
865
865
|
self.c101.code = code
|
866
866
|
self.c104.code = code
|
@@ -879,7 +879,7 @@ class Growth:
|
|
879
879
|
self.c104.page = 'c104q'
|
880
880
|
d2, 매출액증가율_r = self.c104.latest_value_pop2('매출액증가율')
|
881
881
|
|
882
|
-
|
882
|
+
eval_logger.info(f'매출액증가율 : {매출액증가율_r} {매출액증가율_dict}')
|
883
883
|
|
884
884
|
# c106 에서 타 기업과 영업이익률 비교
|
885
885
|
self.c106.page = 'c106y'
|
@@ -913,7 +913,7 @@ class Growth:
|
|
913
913
|
:return:
|
914
914
|
"""
|
915
915
|
redis_name = f"{self.code}_growth"
|
916
|
-
|
916
|
+
eval_logger.info(f"{self} GrowthData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
|
917
917
|
if verbose:
|
918
918
|
print(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
919
919
|
|
analyser_hj3415/tsa.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Time Series Analysis
|
3
3
|
"""
|
4
|
+
import numpy as np
|
4
5
|
import yfinance as yf
|
5
6
|
from datetime import datetime, timedelta
|
6
7
|
import pandas as pd
|
@@ -14,11 +15,20 @@ import matplotlib.pyplot as plt # Matplotlib 수동 임포트
|
|
14
15
|
from db_hj3415 import myredis
|
15
16
|
from collections import OrderedDict
|
16
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
|
17
26
|
|
18
27
|
import logging
|
19
|
-
analyser_logger = helpers.setup_logger('analyser_logger', logging.WARNING)
|
20
28
|
|
21
|
-
|
29
|
+
tsa_logger = helpers.setup_logger('tsa_logger', logging.WARNING)
|
30
|
+
|
31
|
+
expire_time = 3600 * 24
|
22
32
|
|
23
33
|
class MyProphet:
|
24
34
|
def __init__(self, code: str):
|
@@ -39,7 +49,7 @@ class MyProphet:
|
|
39
49
|
@code.setter
|
40
50
|
def code(self, code: str):
|
41
51
|
assert utils.is_6digit(code), f'Invalid value : {code}'
|
42
|
-
|
52
|
+
tsa_logger.info(f'change code : {self.code} -> {code}')
|
43
53
|
self.model = Prophet()
|
44
54
|
self._code = code
|
45
55
|
self.name = myredis.Corps(code, 'c101').get_name()
|
@@ -110,15 +120,24 @@ class MyProphet:
|
|
110
120
|
"""
|
111
121
|
df = self.df_forecast
|
112
122
|
last_real_date = self.df_real.iloc[-1]['ds']
|
113
|
-
|
123
|
+
tsa_logger.info(last_real_date)
|
114
124
|
yhat_dict = df[df['ds']==last_real_date].iloc[0][['ds', 'yhat_lower', 'yhat_upper', 'yhat']].to_dict()
|
115
|
-
|
125
|
+
tsa_logger.info(yhat_dict)
|
116
126
|
return yhat_dict
|
117
127
|
|
118
|
-
def
|
128
|
+
def visualization(self):
|
129
|
+
# 예측 결과 출력
|
130
|
+
print(self.df_forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())
|
131
|
+
# 예측 결과 시각화 (Matplotlib 사용)
|
132
|
+
fig = self.model.plot(self.df_forecast)
|
133
|
+
# 추세 및 계절성 시각화
|
134
|
+
fig2 = self.model.plot_components(self.df_forecast)
|
135
|
+
plt.show() # 시각화 창 띄우기
|
136
|
+
|
137
|
+
def export(self, to="str") -> Optional[str]:
|
119
138
|
"""
|
120
139
|
prophet과 plotly로 그래프를 그려서 html을 문자열로 반환
|
121
|
-
:param to: str, png, htmlfile
|
140
|
+
:param to: str, png, htmlfile
|
122
141
|
:return:
|
123
142
|
"""
|
124
143
|
# Plotly를 사용한 시각화
|
@@ -153,19 +172,12 @@ class MyProphet:
|
|
153
172
|
return graph_html
|
154
173
|
elif to == 'png':
|
155
174
|
# 그래프를 PNG 파일로 저장
|
156
|
-
fig.write_image("
|
175
|
+
fig.write_image(f"myprophet_{self.code}.png")
|
176
|
+
return None
|
157
177
|
elif to == 'htmlfile':
|
158
178
|
# 그래프를 HTML로 저장
|
159
|
-
plot(fig, filename='
|
179
|
+
plot(fig, filename=f'myprophet_{self.code}.html', auto_open=False)
|
160
180
|
return None
|
161
|
-
elif to == 'show':
|
162
|
-
# 예측 결과 출력
|
163
|
-
print(self.df_forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())
|
164
|
-
# 예측 결과 시각화 (Matplotlib 사용)
|
165
|
-
fig = self.model.plot(self.df_forecast)
|
166
|
-
# 추세 및 계절성 시각화
|
167
|
-
fig2 = self.model.plot_components(self.df_forecast)
|
168
|
-
plt.show() # 시각화 창 띄우기
|
169
181
|
else:
|
170
182
|
Exception("to 인자가 맞지 않습니다.")
|
171
183
|
|
@@ -191,7 +203,7 @@ class MyProphet:
|
|
191
203
|
recent_price = last_real_data['y']
|
192
204
|
recent_date = datetime.strftime(last_real_data['ds'], '%Y-%m-%d')
|
193
205
|
yhat_dict = p.get_yhat()
|
194
|
-
|
206
|
+
tsa_logger.info(f'recent_price: {recent_price}, yhat_dict: {yhat_dict}')
|
195
207
|
yhat_lower = int(yhat_dict['yhat_lower'])
|
196
208
|
if recent_price < yhat_lower:
|
197
209
|
deviation = int(eval.Tools.cal_deviation(recent_price, yhat_lower))
|
@@ -203,5 +215,401 @@ class MyProphet:
|
|
203
215
|
|
204
216
|
return OrderedDict(sorted(data_dict.items(), key=lambda item: item[1], reverse=True))
|
205
217
|
|
218
|
+
@dataclass
|
219
|
+
class LSTMData:
|
220
|
+
code: str
|
221
|
+
|
222
|
+
data_2d: np.ndarray
|
223
|
+
train_size: int
|
224
|
+
train_data_2d: np.ndarray
|
225
|
+
test_data_2d: np.ndarray
|
226
|
+
|
227
|
+
X_train_3d: np.ndarray
|
228
|
+
X_test_3d: np.ndarray
|
229
|
+
y_train_1d: np.ndarray
|
230
|
+
y_test_1d: np.ndarray
|
231
|
+
|
232
|
+
@dataclass
|
233
|
+
class LSTMGrade:
|
234
|
+
"""
|
235
|
+
딥러닝 모델의 학습 결과를 평가하기 위해 사용하는 데이터 클래스
|
236
|
+
"""
|
237
|
+
code: str
|
238
|
+
|
239
|
+
mean_train_prediction_2d: np.ndarray
|
240
|
+
mean_test_predictions_2d: np.ndarray
|
241
|
+
|
242
|
+
train_mse: float
|
243
|
+
train_mae: float
|
244
|
+
train_r2: float
|
245
|
+
test_mse: float
|
246
|
+
test_mae: float
|
247
|
+
test_r2: float
|
248
|
+
|
249
|
+
class MyLSTM:
|
250
|
+
"""
|
251
|
+
LSTM(Long Short-Term Memory)
|
252
|
+
"""
|
253
|
+
# 미래 몇일을 예측할 것인가?
|
254
|
+
future_days = 30
|
255
|
+
|
256
|
+
def __init__(self, code: str):
|
257
|
+
assert utils.is_6digit(code), f'Invalid value : {code}'
|
258
|
+
self._code = code
|
259
|
+
self.name = myredis.Corps(code, 'c101').get_name()
|
260
|
+
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
261
|
+
self.raw_data = self._get_raw_data()
|
262
|
+
self.lstm_data = self._preprocessing_for_lstm()
|
263
|
+
|
264
|
+
@property
|
265
|
+
def code(self) -> str:
|
266
|
+
return self._code
|
267
|
+
|
268
|
+
@code.setter
|
269
|
+
def code(self, code: str):
|
270
|
+
assert utils.is_6digit(code), f'Invalid value : {code}'
|
271
|
+
tsa_logger.info(f'change code : {self.code} -> {code}')
|
272
|
+
|
273
|
+
self._code = code
|
274
|
+
self.name = myredis.Corps(code, 'c101').get_name()
|
275
|
+
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
276
|
+
self.raw_data = self._get_raw_data()
|
277
|
+
self.lstm_data = self._preprocessing_for_lstm()
|
278
|
+
|
279
|
+
def _get_raw_data(self) -> pd.DataFrame:
|
280
|
+
"""
|
281
|
+
야후에서 해당 종목의 4년간 주가 raw data를 받아온다.
|
282
|
+
:return:
|
283
|
+
"""
|
284
|
+
# 오늘 날짜 가져오기
|
285
|
+
today = datetime.today()
|
286
|
+
|
287
|
+
# 4년 전 날짜 계산 (4년 = 365일 * 4)
|
288
|
+
four_years_ago = today - timedelta(days=365 * 4)
|
289
|
+
tsa_logger.info(f'start: {four_years_ago.strftime('%Y-%m-%d')}, end: {today.strftime('%Y-%m-%d')}')
|
290
|
+
|
291
|
+
return yf.download(
|
292
|
+
self.code + '.KS',
|
293
|
+
start=four_years_ago.strftime('%Y-%m-%d'),
|
294
|
+
end=today.strftime('%Y-%m-%d')
|
295
|
+
)
|
296
|
+
|
297
|
+
def _preprocessing_for_lstm(self) -> LSTMData:
|
298
|
+
"""
|
299
|
+
lstm이 사용할 수 있도록 데이터 준비(정규화 및 8:2 훈련데이터 검증데이터 분리 및 차원변환)
|
300
|
+
:return:
|
301
|
+
"""
|
302
|
+
# 필요한 열만 선택 (종가만 사용) - 2차웜 배열로 변환
|
303
|
+
data_2d = self.raw_data['Close'].values.reshape(-1, 1)
|
304
|
+
tsa_logger.debug(data_2d)
|
305
|
+
|
306
|
+
# 데이터 정규화 (0과 1 사이로 스케일링)
|
307
|
+
scaled_data_2d = self.scaler.fit_transform(data_2d)
|
308
|
+
|
309
|
+
# 학습 데이터 생성
|
310
|
+
# 주가 데이터를 80%는 학습용, 20%는 테스트용으로 분리하는 코드
|
311
|
+
train_size = int(len(scaled_data_2d) * 0.8)
|
312
|
+
train_data_2d = scaled_data_2d[:train_size]
|
313
|
+
test_data_2d = scaled_data_2d[train_size:]
|
314
|
+
tsa_logger.info(f'총 {len(data_2d)}개 데이터, train size : {train_size}')
|
315
|
+
|
316
|
+
# 학습 데이터에 대한 입력(X)과 정답(y)를 생성
|
317
|
+
def create_dataset(data, time_step=60):
|
318
|
+
X, y = [], []
|
319
|
+
for i in range(len(data) - time_step):
|
320
|
+
X.append(data[i:i + time_step, 0])
|
321
|
+
y.append(data[i + time_step, 0])
|
322
|
+
return np.array(X), np.array(y)
|
323
|
+
|
324
|
+
X_train, y_train_1d = create_dataset(train_data_2d)
|
325
|
+
X_test, y_test_1d = create_dataset(test_data_2d)
|
326
|
+
|
327
|
+
# LSTM 모델 입력을 위해 데이터를 3차원으로 변환
|
328
|
+
X_train_3d = X_train.reshape(X_train.shape[0], X_train.shape[1], 1)
|
329
|
+
X_test_3d = X_test.reshape(X_test.shape[0], X_test.shape[1], 1)
|
330
|
+
|
331
|
+
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}')
|
332
|
+
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)}')
|
333
|
+
|
334
|
+
return LSTMData(
|
335
|
+
code=self.code,
|
336
|
+
data_2d=data_2d,
|
337
|
+
train_size=train_size,
|
338
|
+
train_data_2d=train_data_2d,
|
339
|
+
test_data_2d=test_data_2d,
|
340
|
+
X_train_3d=X_train_3d,
|
341
|
+
X_test_3d=X_test_3d,
|
342
|
+
y_train_1d=y_train_1d,
|
343
|
+
y_test_1d=y_test_1d,
|
344
|
+
)
|
345
|
+
|
346
|
+
def _model_training(self) -> Sequential:
|
347
|
+
# LSTM 모델 생성 - 유닛과 드롭아웃의 수는 테스트로 최적화 됨.
|
348
|
+
model = Sequential()
|
349
|
+
# Input(shape=(50, 1))는 50개의 타임스텝을 가지는 입력 데이터를 처리하며, 각 타임스텝에 1개의 특성이 있다는 것을 의미
|
350
|
+
model.add(Input(shape=(self.lstm_data.X_train_3d.shape[1], 1))) # 입력 레이어에 명시적으로 Input을 사용
|
351
|
+
model.add(LSTM(units=150, return_sequences=True))
|
352
|
+
model.add(Dropout(0.2))
|
353
|
+
model.add(LSTM(units=75, return_sequences=False))
|
354
|
+
model.add(Dropout(0.2))
|
355
|
+
model.add(Dense(units=25))
|
356
|
+
model.add(Dropout(0.3))
|
357
|
+
model.add(Dense(units=1))
|
358
|
+
|
359
|
+
# 모델 요약 출력
|
360
|
+
# model.summary()
|
361
|
+
|
362
|
+
# 모델 컴파일 및 학습
|
363
|
+
model.compile(optimizer='adam', loss='mean_squared_error')
|
364
|
+
|
365
|
+
# 조기 종료 설정
|
366
|
+
early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)
|
367
|
+
|
368
|
+
# 모델 학습 - 과적합 방지위한 조기종료 세팅
|
369
|
+
model.fit(self.lstm_data.X_train_3d, self.lstm_data.y_train_1d,
|
370
|
+
epochs=75, batch_size=32, validation_data=(self.lstm_data.X_test_3d, self.lstm_data.y_test_1d),
|
371
|
+
callbacks=[early_stopping])
|
372
|
+
return model
|
373
|
+
|
374
|
+
def ensemble_training(self, num) -> tuple:
|
375
|
+
"""
|
376
|
+
딥러닝을 num 회 반복하고 평균을 사용하는 함수
|
377
|
+
:param num: 앙상블 모델 수
|
378
|
+
:return:
|
379
|
+
"""
|
380
|
+
def prediction(model_in: Sequential, data: np.ndarray) -> np.ndarray:
|
381
|
+
"""
|
382
|
+
훈련될 모델을 통해 예측을 시행하여 정규화를 복원하고 결과 반환한다.
|
383
|
+
:param model_in:
|
384
|
+
:param data:
|
385
|
+
:return:
|
386
|
+
"""
|
387
|
+
predictions_2d = model_in.predict(data)
|
388
|
+
predictions_scaled_2d = self.scaler.inverse_transform(predictions_2d) # 스케일링 복원
|
389
|
+
tsa_logger.info(f'predictions_scaled_2d : ndim - {predictions_scaled_2d.ndim} len - {len(predictions_scaled_2d)}') # numpy.ndarray 타입
|
390
|
+
tsa_logger.debug(predictions_scaled_2d)
|
391
|
+
return predictions_scaled_2d
|
392
|
+
|
393
|
+
ensemble_train_predictions_2d = []
|
394
|
+
ensemble_test_predictions_2d = []
|
395
|
+
ensemble_future_predictions_2d = []
|
396
|
+
|
397
|
+
for i in range(num):
|
398
|
+
print(f"Training model {i + 1}/{num}...")
|
399
|
+
model = self._model_training()
|
400
|
+
|
401
|
+
# 훈련 데이터 예측
|
402
|
+
train_predictions_scaled_2d = prediction(model, self.lstm_data.X_train_3d)
|
403
|
+
ensemble_train_predictions_2d.append(train_predictions_scaled_2d)
|
404
|
+
|
405
|
+
# 테스트 데이터 예측
|
406
|
+
test_predictions_scaled_2d = prediction(model, self.lstm_data.X_test_3d)
|
407
|
+
ensemble_test_predictions_2d.append(test_predictions_scaled_2d)
|
408
|
+
|
409
|
+
# 8. 미래 30일 예측
|
410
|
+
# 마지막 60일간의 데이터를 기반으로 미래 30일을 예측
|
411
|
+
|
412
|
+
last_60_days_2d = self.lstm_data.test_data_2d[-60:]
|
413
|
+
last_60_days_3d = last_60_days_2d.reshape(1, -1, 1)
|
414
|
+
|
415
|
+
future_predictions = []
|
416
|
+
for _ in range(self.future_days):
|
417
|
+
predicted_price_2d = model.predict(last_60_days_3d)
|
418
|
+
future_predictions.append(predicted_price_2d[0][0])
|
419
|
+
|
420
|
+
# 예측값을 다시 입력으로 사용하여 새로운 예측을 만듦
|
421
|
+
predicted_price_reshaped = np.reshape(predicted_price_2d, (1, 1, 1)) # 3D 배열로 변환
|
422
|
+
last_60_days_3d = np.append(last_60_days_3d[:, 1:, :], predicted_price_reshaped, axis=1)
|
423
|
+
|
424
|
+
# 예측된 주가를 다시 스케일링 복원
|
425
|
+
future_predictions_2d = np.array(future_predictions).reshape(-1, 1)
|
426
|
+
future_predictions_scaled_2d = self.scaler.inverse_transform(future_predictions_2d)
|
427
|
+
ensemble_future_predictions_2d.append(future_predictions_scaled_2d)
|
428
|
+
|
429
|
+
return ensemble_train_predictions_2d, ensemble_test_predictions_2d, ensemble_future_predictions_2d
|
430
|
+
|
431
|
+
def grading(self, ensemble_train_predictions_2d: list, ensemble_test_predictions_2d: list) -> LSTMGrade:
|
432
|
+
"""
|
433
|
+
딥러닝 결과를 분석하기 위한 함수
|
434
|
+
:param ensemble_train_predictions_2d:
|
435
|
+
:param ensemble_test_predictions_2d:
|
436
|
+
:return:
|
437
|
+
"""
|
438
|
+
# 예측값을 평균내서 최종 예측값 도출
|
439
|
+
mean_train_prediction_2d = np.mean(ensemble_train_predictions_2d, axis=0)
|
440
|
+
mean_test_predictions_2d = np.mean(ensemble_test_predictions_2d, axis=0)
|
441
|
+
|
442
|
+
# y값(정답) 정규화 해제
|
443
|
+
y_train_scaled_2d = self.scaler.inverse_transform(self.lstm_data.y_train_1d.reshape(-1, 1))
|
444
|
+
y_test_scaled_2d = self.scaler.inverse_transform(self.lstm_data.y_test_1d.reshape(-1, 1))
|
445
|
+
|
446
|
+
# 평가 지표 계산
|
447
|
+
train_mse = mean_squared_error(y_train_scaled_2d, mean_train_prediction_2d)
|
448
|
+
train_mae = mean_absolute_error(y_train_scaled_2d, mean_train_prediction_2d)
|
449
|
+
train_r2 = r2_score(y_train_scaled_2d, mean_train_prediction_2d)
|
450
|
+
|
451
|
+
test_mse = mean_squared_error(y_test_scaled_2d, mean_test_predictions_2d)
|
452
|
+
test_mae = mean_absolute_error(y_test_scaled_2d, mean_test_predictions_2d)
|
453
|
+
test_r2 = r2_score(y_test_scaled_2d, mean_test_predictions_2d)
|
454
|
+
|
455
|
+
# 평가 결과 출력
|
456
|
+
print("Training Data:")
|
457
|
+
print(f"Train MSE: {train_mse}, Train MAE: {train_mae}, Train R²: {train_r2}")
|
458
|
+
print("\nTesting Data:")
|
459
|
+
print(f"Test MSE: {test_mse}, Test MAE: {test_mae}, Test R²: {test_r2}")
|
460
|
+
# mse, mae는 작을수록 좋으며 R^2은 0-1 사이값 1에 가까울수록 정확함
|
461
|
+
# 과적합에 대한 평가는 train 과 test를 비교하여 test가 너무 않좋으면 과적합 의심.
|
462
|
+
|
463
|
+
return LSTMGrade(
|
464
|
+
code=self.code,
|
465
|
+
mean_train_prediction_2d=mean_train_prediction_2d,
|
466
|
+
mean_test_predictions_2d=mean_test_predictions_2d,
|
467
|
+
train_mse=train_mse,
|
468
|
+
train_mae=train_mae,
|
469
|
+
train_r2=train_r2,
|
470
|
+
test_mse=test_mse,
|
471
|
+
test_mae=test_mae,
|
472
|
+
test_r2=test_r2,
|
473
|
+
)
|
474
|
+
|
475
|
+
def get_final_predictions(self, refresh, num=5) -> tuple:
|
476
|
+
"""
|
477
|
+
미래 예측치를 레디스 캐시를 이용하여 반환함
|
478
|
+
:param refresh:
|
479
|
+
:param num: 앙상블 반복횟수
|
480
|
+
:return:
|
481
|
+
"""
|
482
|
+
print("**** Start get_final_predictions... ****")
|
483
|
+
redis_name = f'{self.code}_mylstm_predictions'
|
484
|
+
|
485
|
+
print(
|
486
|
+
f"redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time / 3600}h")
|
487
|
+
|
488
|
+
def fetch_final_predictions(num_in) -> tuple:
|
489
|
+
"""
|
490
|
+
앙상블법으로 딥러닝을 모델을 반복해서 평균을 내서 미래를 예측한다. 평가는 래시스 캐시로 반환하기 어려워 일단 디버그 용도로만 사용하기로
|
491
|
+
:param num_in:
|
492
|
+
:return:
|
493
|
+
"""
|
494
|
+
# 앙상블 테스트와 채점
|
495
|
+
_, _, ensemble_future_predictions_2d = self.ensemble_training(
|
496
|
+
num=num_in)
|
497
|
+
|
498
|
+
"""if grading:
|
499
|
+
lstm_grade = self.grading(ensemble_train_predictions_2d, ensemble_test_predictions_2d)
|
500
|
+
else:
|
501
|
+
lstm_grade = None"""
|
502
|
+
|
503
|
+
# 시각화를 위한 준비 - 날짜 생성 (미래 예측 날짜), 미래예측값 평균
|
504
|
+
last_date = self.raw_data.index[-1]
|
505
|
+
future_dates = pd.date_range(last_date, periods=self.future_days + 1).tolist()[1:]
|
506
|
+
|
507
|
+
# Timestamp 객체를 문자열로 변환
|
508
|
+
future_dates_str= [date.strftime('%Y-%m-%d') for date in future_dates]
|
509
|
+
|
510
|
+
final_future_predictions = np.mean(ensemble_future_predictions_2d, axis=0)
|
511
|
+
tsa_logger.info(f'num - future dates : {len(future_dates_str)} future data : {len(final_future_predictions)}')
|
512
|
+
|
513
|
+
assert len(future_dates_str) == len(final_future_predictions), "future_dates 와 final_future_predictions 개수가 일치하지 않습니다."
|
514
|
+
|
515
|
+
return future_dates_str, final_future_predictions.tolist()
|
516
|
+
|
517
|
+
future_dates_str, final_future_predictions = myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_final_predictions, num, timer=expire_time)
|
518
|
+
|
519
|
+
# 문자열을 날짜 형식으로 변환
|
520
|
+
future_dates = [datetime.strptime(date, '%Y-%m-%d') for date in future_dates_str]
|
521
|
+
|
522
|
+
# 리스트를 다시 NumPy 배열로 변환
|
523
|
+
final_future_predictions = np.array(final_future_predictions)
|
524
|
+
|
525
|
+
return future_dates, final_future_predictions
|
526
|
+
|
527
|
+
def export(self, refresh=False, to="str") -> Optional[str]:
|
528
|
+
"""
|
529
|
+
prophet과 plotly로 그래프를 그려서 html을 문자열로 반환
|
530
|
+
:param refresh:
|
531
|
+
:param to: str, htmlfile, png
|
532
|
+
:return:
|
533
|
+
"""
|
534
|
+
future_dates, final_future_predictions = self.get_final_predictions(refresh=refresh)
|
535
|
+
final_future_predictions = final_future_predictions.reshape(-1) # 차원을 하나 줄인다.
|
536
|
+
|
537
|
+
# Plotly를 사용한 시각화
|
538
|
+
fig = go.Figure()
|
539
|
+
|
540
|
+
# 실제 데이터
|
541
|
+
fig.add_trace(go.Scatter(x=self.raw_data.index[-120:], y=self.raw_data['Close'][-120:], mode='markers', name='실제주가'))
|
542
|
+
tsa_logger.debug(f"self.raw_data.index[-120:] - {self.raw_data.index[-120:]}")
|
543
|
+
tsa_logger.debug(f"self.raw_data['Close'][-120:] - {self.raw_data['Close'][-120:]}")
|
544
|
+
# 예측 데이터
|
545
|
+
fig.add_trace(go.Scatter(x=future_dates, y=final_future_predictions, mode='lines+markers', name='예측치(30일)'))
|
546
|
+
tsa_logger.debug(f"future_dates - {future_dates}")
|
547
|
+
tsa_logger.debug(f"final_future_predictions - {final_future_predictions}")
|
548
|
+
|
549
|
+
fig.update_layout(
|
550
|
+
# title=f'{self.code} {self.name} 주가 예측 그래프(prophet)',
|
551
|
+
xaxis_title='일자',
|
552
|
+
yaxis_title='주가(원)',
|
553
|
+
xaxis = dict(
|
554
|
+
tickformat='%Y/%m', # X축을 '연/월' 형식으로 표시
|
555
|
+
),
|
556
|
+
yaxis = dict(
|
557
|
+
tickformat=".0f", # 소수점 없이 원래 숫자 표시
|
558
|
+
)
|
559
|
+
)
|
560
|
+
|
561
|
+
if to == 'str':
|
562
|
+
# 그래프 HTML로 변환 (string 형식으로 저장)
|
563
|
+
graph_html = plot(fig, output_type='div')
|
564
|
+
return graph_html
|
565
|
+
elif to == 'png':
|
566
|
+
# 그래프를 PNG 파일로 저장
|
567
|
+
fig.write_image(f"myLSTM_{self.code}.png")
|
568
|
+
return None
|
569
|
+
elif to == 'htmlfile':
|
570
|
+
# 그래프를 HTML로 저장
|
571
|
+
plot(fig, filename=f'myLSTM_{self.code}.html', auto_open=False)
|
572
|
+
return None
|
573
|
+
else:
|
574
|
+
Exception("to 인자가 맞지 않습니다.")
|
575
|
+
|
576
|
+
def visualization(self, refresh=True):
|
577
|
+
future_dates, final_future_predictions = self.get_final_predictions(refresh=refresh)
|
578
|
+
|
579
|
+
# 시각화1
|
580
|
+
plt.figure(figsize=(10, 6))
|
581
|
+
|
582
|
+
# 실제 주가
|
583
|
+
plt.plot(self.raw_data.index, self.raw_data['Close'], label='Actual Price')
|
584
|
+
|
585
|
+
# 미래 주가 예측
|
586
|
+
plt.plot(future_dates, final_future_predictions, label='Future Predicted Price', linestyle='--')
|
587
|
+
|
588
|
+
plt.xlabel('Date')
|
589
|
+
plt.ylabel('Stock Price')
|
590
|
+
plt.legend()
|
591
|
+
plt.title('Apple Stock Price Prediction with LSTM')
|
592
|
+
plt.show()
|
593
|
+
|
594
|
+
"""# 시각화2
|
595
|
+
plt.figure(figsize=(10, 6))
|
596
|
+
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')
|
597
|
+
plt.plot(self.raw_data.index[self.lstm_data.train_size + 60:], lstm_grade.mean_test_predictions_2d, label='Predicted Price')
|
598
|
+
plt.xlabel('Date')
|
599
|
+
plt.ylabel('Price')
|
600
|
+
plt.legend()
|
601
|
+
plt.title('Stock Price Prediction with LSTM Ensemble')
|
602
|
+
plt.show()"""
|
603
|
+
|
604
|
+
def caching_based_on_prophet_ranking(self, refresh: bool, top=20):
|
605
|
+
ranking_topn = OrderedDict(itertools.islice(MyProphet.ranking().items(), top))
|
606
|
+
tsa_logger.info(ranking_topn)
|
607
|
+
print(f"*** LSTM prediction redis cashing top{top} items ***")
|
608
|
+
for i, (code, _) in enumerate(ranking_topn.items()):
|
609
|
+
print(f"{i+1}. {self.code}/{self.name}")
|
610
|
+
self.code = code
|
611
|
+
self.get_final_predictions(refresh=refresh, num=5)
|
612
|
+
|
613
|
+
|
206
614
|
|
207
615
|
|
@@ -1,16 +1,15 @@
|
|
1
1
|
analyser_hj3415/.DS_Store,sha256=qr9-0FPn5CFKe6kEu8_dWCNhzQ0sN7bwQgffKsaJEEo,6148
|
2
2
|
analyser_hj3415/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
analyser_hj3415/cli.py,sha256=
|
4
|
-
analyser_hj3415/eval.py,sha256=
|
5
|
-
analyser_hj3415/tsa.py,sha256=
|
3
|
+
analyser_hj3415/cli.py,sha256=EW0-lIrpZHNNeDKksxC4qDBCiHPkYMGMHsZhAg6VsS8,12870
|
4
|
+
analyser_hj3415/eval.py,sha256=WWIvB4BebjW9GNGcF8rMd-MLL-lPXUBOH01_FpSq95I,38811
|
5
|
+
analyser_hj3415/tsa.py,sha256=fAC4hsqWMkzoHrRr9Ulcq0s6lVfbQE7vTP_ulJmXkcQ,25097
|
6
6
|
analyser_hj3415/workroom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
analyser_hj3415/workroom/lstm.py,sha256=b0ICzIVeCODcH4tAkVyrHVJLXwf21wmkIambKPHaZMQ,4155
|
8
7
|
analyser_hj3415/workroom/mysklearn.py,sha256=wJXKz5MqqTzADdG2mqRMMzc_G9RzwYjj5_j4gyOopxQ,2030
|
9
8
|
analyser_hj3415/workroom/mysklearn2.py,sha256=1lIy6EWEQHkOzDS-av8U0zQH6DuCLKWMI73dnJx5KRs,1495
|
10
9
|
analyser_hj3415/workroom/score.py,sha256=P6nHBJYmyhigGtT4qna4BmNtvt4B93b7SKyzdstJK24,17376
|
11
10
|
analyser_hj3415/workroom/trash.py,sha256=zF-W0piqkGr66UP6-iybo9EXh2gO0RP6R1FnIpsGkl8,12262
|
12
|
-
analyser_hj3415-2.
|
13
|
-
analyser_hj3415-2.
|
14
|
-
analyser_hj3415-2.
|
15
|
-
analyser_hj3415-2.
|
16
|
-
analyser_hj3415-2.
|
11
|
+
analyser_hj3415-2.9.0.dist-info/entry_points.txt,sha256=ZfjPnJuH8SzvhE9vftIPMBIofsc65IAWYOhqOC_L5ck,65
|
12
|
+
analyser_hj3415-2.9.0.dist-info/LICENSE,sha256=QVKTp0dTnB5xG8RLgG17LwSWCKNEzYoVVM6KjoCPKc0,1079
|
13
|
+
analyser_hj3415-2.9.0.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
|
14
|
+
analyser_hj3415-2.9.0.dist-info/METADATA,sha256=i3FwxTCib11QDlFyxVKM_mKBxH8fXmTxP82gjttoiYE,6607
|
15
|
+
analyser_hj3415-2.9.0.dist-info/RECORD,,
|
analyser_hj3415/workroom/lstm.py
DELETED
@@ -1,115 +0,0 @@
|
|
1
|
-
import yfinance as yf
|
2
|
-
import numpy as np
|
3
|
-
import pandas as pd
|
4
|
-
from sklearn.preprocessing import MinMaxScaler
|
5
|
-
from tensorflow.keras.models import Sequential
|
6
|
-
from tensorflow.keras.layers import LSTM, Dense, Dropout
|
7
|
-
import matplotlib.pyplot as plt
|
8
|
-
|
9
|
-
# 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='2019-01-01', end='2024-10-11')
|
13
|
-
# 크래프톤 주식 데이터 가져오기 (KOSPI 상장)
|
14
|
-
stock_data = yf.download('259960.KS', start='2020-01-01', end='2024-10-11')
|
15
|
-
# 하이닉스 주식 데이터 가져오기 (KOSPI 상장)
|
16
|
-
#stock_data = yf.download('000660.KS', start='2019-01-01', end='2024-10-11')
|
17
|
-
|
18
|
-
stock_data = yf.download('004490.KS', start='2020-01-01', end='2024-10-15')
|
19
|
-
|
20
|
-
|
21
|
-
# 2. 필요한 열만 선택 (종가만 사용)
|
22
|
-
data = stock_data['Close'].values.reshape(-1, 1)
|
23
|
-
|
24
|
-
# 3. 데이터 정규화 (0과 1 사이로 스케일링)
|
25
|
-
scaler = MinMaxScaler(feature_range=(0, 1))
|
26
|
-
scaled_data = scaler.fit_transform(data)
|
27
|
-
|
28
|
-
# 4. 학습 데이터 생성
|
29
|
-
# 주가 데이터를 80%는 학습용, 20%는 테스트용으로 분리하는 코드
|
30
|
-
train_size = int(len(scaled_data) * 0.8)
|
31
|
-
train_data = scaled_data[:train_size]
|
32
|
-
test_data = scaled_data[train_size:]
|
33
|
-
|
34
|
-
|
35
|
-
# 학습 데이터에 대한 입력(X)과 출력(y)를 생성
|
36
|
-
def create_dataset(data, time_step=60):
|
37
|
-
X, y = [], []
|
38
|
-
for i in range(len(data) - time_step):
|
39
|
-
X.append(data[i:i + time_step, 0])
|
40
|
-
y.append(data[i + time_step, 0])
|
41
|
-
return np.array(X), np.array(y)
|
42
|
-
|
43
|
-
|
44
|
-
X_train, y_train = create_dataset(train_data)
|
45
|
-
X_test, y_test = create_dataset(test_data)
|
46
|
-
|
47
|
-
# LSTM 모델 입력을 위해 데이터를 3차원으로 변환
|
48
|
-
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], 1)
|
49
|
-
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], 1)
|
50
|
-
|
51
|
-
# 5. LSTM 모델 생성
|
52
|
-
model = Sequential()
|
53
|
-
model.add(LSTM(units=200, return_sequences=True, input_shape=(X_train.shape[1], 1)))
|
54
|
-
model.add(Dropout(0.2))
|
55
|
-
model.add(LSTM(units=100, return_sequences=False))
|
56
|
-
model.add(Dropout(0.2))
|
57
|
-
model.add(Dense(units=50))
|
58
|
-
model.add(Dropout(0.3))
|
59
|
-
model.add(Dense(units=1))
|
60
|
-
|
61
|
-
# 6. 모델 컴파일 및 학습
|
62
|
-
model.compile(optimizer='adam', loss='mean_squared_error')
|
63
|
-
model.fit(X_train, y_train, epochs=20, batch_size=32)
|
64
|
-
|
65
|
-
# 7. 테스트 데이터 예측
|
66
|
-
predictions = model.predict(X_test)
|
67
|
-
predictions = scaler.inverse_transform(predictions) # 스케일링 복원
|
68
|
-
|
69
|
-
# 8. 미래 30일 예측
|
70
|
-
# 마지막 60일간의 데이터를 기반으로 미래 30일을 예측
|
71
|
-
future_days = 30
|
72
|
-
last_60_days = test_data[-60:]
|
73
|
-
last_60_days = last_60_days.reshape(1, -1, 1)
|
74
|
-
|
75
|
-
future_predictions = []
|
76
|
-
for _ in range(future_days):
|
77
|
-
predicted_price = model.predict(last_60_days)
|
78
|
-
future_predictions.append(predicted_price[0][0])
|
79
|
-
|
80
|
-
# 예측값을 다시 입력으로 사용하여 새로운 예측을 만듦
|
81
|
-
predicted_price_reshaped = np.reshape(predicted_price, (1, 1, 1)) # 3D 배열로 변환
|
82
|
-
last_60_days = np.append(last_60_days[:, 1:, :], predicted_price_reshaped, axis=1)
|
83
|
-
|
84
|
-
# 예측된 주가를 다시 스케일링 복원
|
85
|
-
future_predictions = np.array(future_predictions).reshape(-1, 1)
|
86
|
-
future_predictions = scaler.inverse_transform(future_predictions)
|
87
|
-
|
88
|
-
# 9. 날짜 생성 (미래 예측 날짜)
|
89
|
-
last_date = stock_data.index[-1]
|
90
|
-
future_dates = pd.date_range(last_date, periods=future_days + 1).tolist()[1:]
|
91
|
-
|
92
|
-
# 10. 시각화
|
93
|
-
plt.figure(figsize=(10, 6))
|
94
|
-
|
95
|
-
# 실제 주가
|
96
|
-
plt.plot(stock_data.index, stock_data['Close'], label='Actual Price')
|
97
|
-
|
98
|
-
# 미래 주가 예측
|
99
|
-
plt.plot(future_dates, future_predictions, label='Future Predicted Price', linestyle='--')
|
100
|
-
|
101
|
-
plt.xlabel('Date')
|
102
|
-
plt.ylabel('Stock Price')
|
103
|
-
plt.legend()
|
104
|
-
plt.title('Apple Stock Price Prediction with LSTM')
|
105
|
-
plt.show()
|
106
|
-
|
107
|
-
# 8. 시각화
|
108
|
-
plt.figure(figsize=(10, 6))
|
109
|
-
plt.plot(stock_data.index[train_size + 60:], data[train_size + 60:], label='Actual Price')
|
110
|
-
plt.plot(stock_data.index[train_size + 60:], predictions, label='Predicted Price')
|
111
|
-
plt.xlabel('Date')
|
112
|
-
plt.ylabel('Price')
|
113
|
-
plt.legend()
|
114
|
-
plt.title('Apple Stock Price Prediction with LSTM')
|
115
|
-
plt.show()
|
File without changes
|
File without changes
|
File without changes
|