analyser_hj3415 2.8.2__py2.py3-none-any.whl → 2.9.0__py2.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/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
|