analyser_hj3415 2.9.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,960 @@
1
+ from dataclasses import dataclass, asdict
2
+
3
+ from utils_hj3415 import utils, helpers
4
+ from typing import Tuple
5
+ from db_hj3415 import myredis, mymongo
6
+ import math
7
+ from analyser_hj3415.cli import AnalyserSettingsManager
8
+ from collections import OrderedDict
9
+ import logging
10
+
11
+ eval_logger = helpers.setup_logger('eval_logger', logging.WARNING)
12
+
13
+ expire_time = 3600 * 12
14
+
15
+ class Tools:
16
+ @staticmethod
17
+ def cal_deviation(v1: float, v2: float) -> float:
18
+ """
19
+ 괴리율 구하는 공식
20
+ :param v1:
21
+ :param v2:
22
+ :return:
23
+ """
24
+ try:
25
+ deviation = abs((v1 - v2) / v1) * 100
26
+ except ZeroDivisionError:
27
+ deviation = math.nan
28
+ return deviation
29
+
30
+ @staticmethod
31
+ def date_set(*args) -> list:
32
+ """
33
+ 비유효한 내용 제거(None,nan)하고 중복된 항목 제거하고 리스트로 반환한다.
34
+ 여기서 set의 의미는 집합을 뜻함
35
+ :param args:
36
+ :return:
37
+ """
38
+ return [i for i in {*args} if i != "" and i is not math.nan and i is not None]
39
+
40
+ @staticmethod
41
+ def calc당기순이익(c103: myredis.C103, refresh: bool) -> Tuple[str, float]:
42
+ """
43
+ 지배지분 당기순이익 계산
44
+
45
+ 일반적인 경우로는 직전 지배주주지분 당기순이익을 찾아서 반환한다.\n
46
+ 금융기관의 경우는 지배당기순이익이 없기 때문에\n
47
+ 계산을 통해서 간접적으로 구한다.\n
48
+ """
49
+ name = myredis.Corps(c103.code, 'c101').get_name(refresh=refresh)
50
+
51
+ eval_logger.info(f'{c103.code} / {name} Tools : 당기순이익 계산.. refresh : {refresh}')
52
+ c103.page = 'c103재무상태표q'
53
+
54
+ d1, 지배당기순이익 = c103.latest_value_pop2('*(지배)당기순이익', refresh)
55
+ eval_logger.debug(f"*(지배)당기순이익: {지배당기순이익}")
56
+
57
+ if math.isnan(지배당기순이익):
58
+ eval_logger.warning(f"{c103.code} / {name} - (지배)당기순이익이 없는 종목. 수동으로 계산합니다.")
59
+ c103.page = 'c103손익계산서q'
60
+ d2, 최근4분기당기순이익 = c103.sum_recent_4q('당기순이익', refresh)
61
+ eval_logger.debug(f"{c103.code} / {name} - 최근4분기당기순이익 : {최근4분기당기순이익}")
62
+ c103.page = 'c103재무상태표y'
63
+ d3, 비지배당기순이익 = c103.latest_value_pop2('*(비지배)당기순이익', refresh)
64
+ eval_logger.debug(f"{c103.code} / {name} - 비지배당기순이익y : {비지배당기순이익}")
65
+ # 가변리스트 언패킹으로 하나의 날짜만 사용하고 나머지는 버린다.
66
+ # 여기서 *_는 “나머지 값을 다 무시하겠다”는 의미
67
+ eval_logger.debug(f"d2:{d2}, d3: {d3}")
68
+ try:
69
+ date, *_ = Tools.date_set(d2, d3)
70
+ except ValueError:
71
+ # 날짜 데이터가 없는경우
72
+ date = ''
73
+ 계산된지배당기순이익= round(최근4분기당기순이익 - utils.nan_to_zero(비지배당기순이익), 1)
74
+ eval_logger.debug(f"{c103.code} / {name} - 계산된 지배당기순이익 : {계산된지배당기순이익}")
75
+ return date, 계산된지배당기순이익
76
+ else:
77
+ return d1, 지배당기순이익
78
+
79
+ @staticmethod
80
+ def calc유동자산(c103: myredis.C103, refresh: bool) -> Tuple[str, float]:
81
+ """유효한 유동자산 계산
82
+
83
+ 일반적인 경우로 유동자산을 찾아서 반환한다.\n
84
+ 금융기관의 경우는 간접적으로 계산한다.\n
85
+ """
86
+ name = myredis.Corps(c103.code, 'c101').get_name(refresh=refresh)
87
+
88
+ eval_logger.info(f'{c103.code} / {name} Tools : 유동자산계산... refresh : {refresh}')
89
+ c103.page = 'c103재무상태표q'
90
+
91
+ d, 유동자산 = c103.sum_recent_4q('유동자산', refresh)
92
+ if math.isnan(유동자산):
93
+ eval_logger.warning(f"{c103.code} / {name} - 유동자산이 없는 종목. 수동으로 계산합니다(금융관련업종일 가능성있음).")
94
+ d1, v1 = c103.latest_value_pop2('현금및예치금', refresh)
95
+ d2, v2 = c103.latest_value_pop2('단기매매금융자산', refresh)
96
+ d3, v3 = c103.latest_value_pop2('매도가능금융자산', refresh)
97
+ d4, v4 = c103.latest_value_pop2('만기보유금융자산', refresh)
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
+
103
+ try:
104
+ date, *_ = Tools.date_set(d1, d2, d3, d4)
105
+ except ValueError:
106
+ # 날짜 데이터가 없는경우
107
+ date = ''
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
+
110
+ eval_logger.info(f"{c103.code} / {name} - 계산된 유동자산 : {계산된유동자산value}")
111
+ return date, 계산된유동자산value
112
+ else:
113
+ return d, 유동자산
114
+
115
+ @staticmethod
116
+ def calc유동부채(c103: myredis.C103, refresh: bool) -> Tuple[str, float]:
117
+ """유효한 유동부채 계산
118
+
119
+ 일반적인 경우로 유동부채를 찾아서 반환한다.\n
120
+ 금융기관의 경우는 간접적으로 계산한다.\n
121
+ """
122
+ name = myredis.Corps(c103.code, 'c101').get_name(refresh=refresh)
123
+
124
+ eval_logger.info(f'{c103.code} / {name} Tools : 유동부채계산... refresh : {refresh}')
125
+ c103.page = 'c103재무상태표q'
126
+
127
+ d, 유동부채 = c103.sum_recent_4q('유동부채', refresh)
128
+ if math.isnan(유동부채):
129
+ eval_logger.warning(f"{c103.code} / {name} - 유동부채가 없는 종목. 수동으로 계산합니다.")
130
+ d1, v1 = c103.latest_value_pop2('당기손익인식(지정)금융부채', refresh)
131
+ d2, v2 = c103.latest_value_pop2('당기손익-공정가치측정금융부채', refresh)
132
+ d3, v3 = c103.latest_value_pop2('매도파생결합증권', refresh)
133
+ d4, v4 = c103.latest_value_pop2('단기매매금융부채', refresh)
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
+
139
+ try:
140
+ date, *_ = Tools.date_set(d1, d2, d3, d4)
141
+ except ValueError:
142
+ # 날짜 데이터가 없는경우
143
+ date = ''
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
+
146
+ eval_logger.info(f"{c103.code} / {name} - 계산된 유동부채 : {계산된유동부채value}")
147
+ return date, 계산된유동부채value
148
+ else:
149
+ return d, 유동부채
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+ @dataclass
158
+ class RedData:
159
+ code: str
160
+ name: str
161
+
162
+ # 사업가치 계산 - 지배주주지분 당기순이익 / 기대수익률
163
+ 사업가치: float
164
+ 지배주주당기순이익: float
165
+ expect_earn: float
166
+
167
+ # 재산가치 계산 - 유동자산 - (유동부채*1.2) + 고정자산중 투자자산
168
+ 재산가치: float
169
+ 유동자산: float
170
+ 유동부채: float
171
+ 투자자산: float
172
+ 투자부동산: float
173
+
174
+ # 부채평가 - 비유동부채
175
+ 부채평가: float
176
+
177
+ # 발행주식수
178
+ 발행주식수: int
179
+
180
+ date: list
181
+
182
+ red_price: float
183
+ score: int
184
+
185
+ def __post_init__(self):
186
+ if not utils.is_6digit(self.code):
187
+ raise ValueError(f"code는 6자리 숫자형 문자열이어야합니다. (입력값: {self.code})")
188
+
189
+
190
+ class Red:
191
+ expect_earn = float(AnalyserSettingsManager().get_value('EXPECT_EARN'))
192
+
193
+ def __init__(self, code: str):
194
+ assert utils.is_6digit(code), f'Invalid value : {code}'
195
+ eval_logger.debug(f"Red : 초기화 ({code})")
196
+ self.c101 = myredis.C101(code)
197
+ self.c103 = myredis.C103(code, 'c103재무상태표q')
198
+
199
+ self.name = self.c101.get_name()
200
+ self._code = code
201
+
202
+ def __str__(self):
203
+ return f"Red({self.code}/{self.name})"
204
+
205
+ @property
206
+ def code(self) -> str:
207
+ return self._code
208
+
209
+ @code.setter
210
+ def code(self, code: str):
211
+ assert utils.is_6digit(code), f'Invalid value : {code}'
212
+ eval_logger.debug(f"Red : 종목코드 변경({self.code} -> {code})")
213
+ self.c101.code = code
214
+ self.c103.code = code
215
+
216
+ self.name = self.c101.get_name()
217
+ self._code = code
218
+
219
+ def _calc비유동부채(self, refresh: bool) -> Tuple[str, float]:
220
+ """유효한 비유동부채 계산
221
+
222
+ 일반적인 경우로 비유동부채를 찾아서 반환한다.\n
223
+ 금융기관의 경우는 간접적으로 계산한다.\n
224
+ """
225
+ eval_logger.info(f'In the calc비유동부채... refresh : {refresh}')
226
+ self.c103.page = 'c103재무상태표q'
227
+
228
+ d, 비유동부채 = self.c103.sum_recent_4q('비유동부채', refresh)
229
+ if math.isnan(비유동부채):
230
+ eval_logger.warning(f"{self} - 비유동부채가 없는 종목. 수동으로 계산합니다.")
231
+ # 보험관련업종은 예수부채가 없는대신 보험계약부채가 있다...
232
+ d1, v1 = self.c103.latest_value_pop2('예수부채', refresh)
233
+ d2, v2 = self.c103.latest_value_pop2('보험계약부채(책임준비금)', refresh)
234
+ d3, v3 = self.c103.latest_value_pop2('차입부채', refresh)
235
+ d4, v4 = self.c103.latest_value_pop2('기타부채', refresh)
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
+
241
+ try:
242
+ date, *_ = Tools.date_set(d1, d2, d3, d4)
243
+ except ValueError:
244
+ # 날짜 데이터가 없는경우
245
+ date = ''
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
+ eval_logger.info(f"{self} - 계산된 비유동부채 : {계산된비유동부채value}")
248
+ return date, 계산된비유동부채value
249
+ else:
250
+ return d, 비유동부채
251
+
252
+ def _score(self, red_price: int, refresh: bool) -> int:
253
+ """red price와 최근 주가의 괴리율 파악
254
+
255
+ Returns:
256
+ int : 주가와 red price 비교한 괴리율
257
+ """
258
+ try:
259
+ recent_price = utils.to_int(self.c101.get_recent(refresh)['주가'])
260
+ except KeyError:
261
+ return 0
262
+
263
+ deviation = Tools.cal_deviation(recent_price, red_price)
264
+ if red_price < 0 or (recent_price >= red_price):
265
+ score = 0
266
+ else:
267
+ score = utils.to_int(math.log10(deviation + 1) * 33) # desmos그래프상 33이 제일 적당한듯(최대100점에 가깝게)
268
+
269
+ eval_logger.debug(f"최근주가 : {recent_price} red가격 : {red_price} 괴리율 : {utils.to_int(deviation)} score : {score}")
270
+
271
+ return score
272
+
273
+ def _generate_data(self, refresh: bool) -> RedData:
274
+ d1, 지배주주당기순이익 = Tools.calc당기순이익(self.c103, refresh)
275
+ eval_logger.debug(f"{self} 지배주주당기순이익: {지배주주당기순이익}")
276
+ d2, 유동자산 = Tools.calc유동자산(self.c103, refresh)
277
+ d3, 유동부채 = Tools.calc유동부채(self.c103, refresh)
278
+ d4, 부채평가 = self._calc비유동부채(refresh)
279
+
280
+ self.c103.page = 'c103재무상태표q'
281
+ d5, 투자자산 = self.c103.latest_value_pop2('투자자산', refresh)
282
+ d6, 투자부동산 = self.c103.latest_value_pop2('투자부동산', refresh)
283
+
284
+ # 사업가치 계산 - 지배주주지분 당기순이익 / 기대수익률
285
+ 사업가치 = round(지배주주당기순이익 / Red.expect_earn, 2)
286
+
287
+ # 재산가치 계산 - 유동자산 - (유동부채*1.2) + 고정자산중 투자자산
288
+ 재산가치 = round(유동자산 - (유동부채 * 1.2) + utils.nan_to_zero(투자자산) + utils.nan_to_zero(투자부동산), 2)
289
+
290
+ _, 발행주식수 = self.c103.latest_value_pop2('발행주식수', refresh)
291
+ if math.isnan(발행주식수):
292
+ 발행주식수 = utils.to_int(self.c101.get_recent(refresh).get('발행주식'))
293
+ else:
294
+ 발행주식수 = 발행주식수 * 1000
295
+
296
+ try:
297
+ red_price = round(((사업가치 + 재산가치 - 부채평가) * 100000000) / 발행주식수)
298
+ except (ZeroDivisionError, ValueError):
299
+ red_price = math.nan
300
+
301
+ score = self._score(red_price, refresh)
302
+
303
+ try:
304
+ date_list = Tools.date_set(d1, d2, d3, d4)
305
+ except ValueError:
306
+ # 날짜 데이터가 없는경우
307
+ date_list = ['',]
308
+
309
+ return RedData(
310
+ code = self.code,
311
+ name = self.name,
312
+ 사업가치 = 사업가치,
313
+ 지배주주당기순이익 = 지배주주당기순이익,
314
+ expect_earn = Red.expect_earn,
315
+ 재산가치 = 재산가치,
316
+ 유동자산 = 유동자산,
317
+ 유동부채 = 유동부채,
318
+ 투자자산 = 투자자산,
319
+ 투자부동산 = 투자부동산,
320
+ 부채평가 = 부채평가,
321
+ 발행주식수 = 발행주식수,
322
+ date = date_list,
323
+ red_price = red_price,
324
+ score = score,
325
+ )
326
+
327
+ def get(self, refresh = False, verbose = True) -> RedData:
328
+ """
329
+ RedData 형식의 데이터를 계산하여 리턴하고 레디스 캐시에 저장한다.
330
+ :param refresh:
331
+ :return:
332
+ """
333
+ redis_name = f"{self.code}_red"
334
+ eval_logger.info(f"{self} RedData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
335
+ expire_time = 3600 * 12
336
+ if verbose:
337
+ print(f"{self} redisname: '{redis_name}' / expect_earn: {Red.expect_earn} / refresh : {refresh} / expire_time : {expire_time/3600}h")
338
+
339
+ def fetch_generate_data(refresh_in: bool) -> dict:
340
+ return asdict(self._generate_data(refresh_in))
341
+
342
+ return RedData(**myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_data, refresh, timer=expire_time))
343
+
344
+ @classmethod
345
+ def ranking(cls, expect_earn: float = None, refresh = False) -> OrderedDict:
346
+ """
347
+ redis를 사용하며 red score를 계산해서 0이상의 값을 가지는 종목을 순서대로 저장하여 반환한다.
348
+ :param expect_earn: 기대수익률(일반적으로 0.06 - 0.10)
349
+ :param refresh: 캐시를 사용하지 않고 강제로 다시 계산
350
+ :return: OrderedDict([('023590', 101),
351
+ ('010060', 91),...]), 레디스이름
352
+ """
353
+
354
+ print("**** Start red_ranking... ****")
355
+ # expect_earn 및 refresh 설정
356
+ if expect_earn is None:
357
+ expect_earn = cls.expect_earn
358
+ eval_logger.info(f"기대수익률을 {expect_earn}으로 설정합니다.")
359
+ previous_expect_earn = float(AnalyserSettingsManager().get_value('RED_RANKING_EXPECT_EARN'))
360
+ eval_logger.debug(f"previous red ranking expect earn : {previous_expect_earn}")
361
+ if previous_expect_earn != expect_earn:
362
+ eval_logger.warning(f"expect earn : {expect_earn} / RED_RANKING_EXPECT_EARN : {previous_expect_earn} 두 값이 달라 refresh = True")
363
+ refresh = True
364
+
365
+ redis_name = 'red_ranking'
366
+
367
+ print(f"redisname: '{redis_name}' / expect_earn: {expect_earn} / refresh : {refresh} / expire_time : {expire_time/3600}h")
368
+
369
+ def fetch_ranking(expect_earn_in: float, refresh_in: bool) -> dict:
370
+ data = {}
371
+ # 저장된 기대수익률을 불러서 임시저장
372
+ ee_orig = Red.expect_earn
373
+ # 원하는 기대수익률로 클래스 세팅
374
+ Red.expect_earn = expect_earn_in
375
+ AnalyserSettingsManager().set_value('RED_RANKING_EXPECT_EARN', str(expect_earn_in))
376
+ red = Red('005930')
377
+ for i, code in enumerate(myredis.Corps.list_all_codes()):
378
+ red.code = code
379
+ red_score = red.get(refresh=refresh_in, verbose=False).score
380
+ if red_score > 0:
381
+ data[code] = red_score
382
+ print(f"{i}: {red} - {red_score}")
383
+ # 원래 저장되었던 기대수익률로 다시 복원
384
+ Red.expect_earn = ee_orig
385
+ return data
386
+
387
+ data_dict = myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_ranking, expect_earn, refresh, timer=expire_time)
388
+
389
+ return OrderedDict(sorted(data_dict.items(), key=lambda item: item[1], reverse=True))
390
+
391
+
392
+ @dataclass
393
+ class MilData:
394
+ code: str
395
+ name: str
396
+
397
+ 시가총액억: float
398
+
399
+ 주주수익률: float
400
+ 재무활동현금흐름: float
401
+
402
+ 이익지표: float
403
+ 영업활동현금흐름: float
404
+ 지배주주당기순이익: float
405
+
406
+ #투자수익률
407
+ roic_r: float
408
+ roic_dict: dict
409
+ roe_r: float
410
+ roe_106: dict
411
+ roa_r: float
412
+
413
+ #가치지표
414
+ fcf_dict: dict
415
+ pfcf_dict: dict
416
+ pcr_dict: dict
417
+
418
+ score: list
419
+ date: list
420
+
421
+
422
+ class Mil:
423
+ def __init__(self, code: str):
424
+ assert utils.is_6digit(code), f'Invalid value : {code}'
425
+ eval_logger.debug(f"Mil : 종목코드 ({code})")
426
+
427
+ self.c101 = myredis.C101(code)
428
+ self.c103 = myredis.C103(code, 'c103현금흐름표q')
429
+ self.c104 = myredis.C104(code, 'c104q')
430
+ self.c106 = myredis.C106(code, 'c106q')
431
+
432
+ self.name = self.c101.get_name()
433
+ self._code = code
434
+
435
+ def __str__(self):
436
+ return f"Mil({self.code}/{self.name})"
437
+
438
+ @property
439
+ def code(self) -> str:
440
+ return self._code
441
+
442
+ @code.setter
443
+ def code(self, code: str):
444
+ assert utils.is_6digit(code), f'Invalid value : {code}'
445
+ eval_logger.debug(f"Mil : 종목코드 변경({self.code} -> {code})")
446
+
447
+ self.c101.code = code
448
+ self.c103.code = code
449
+ self.c104.code = code
450
+ self.c106.code = code
451
+
452
+ self.name = self.c101.get_name()
453
+ self._code = code
454
+
455
+ def get_marketcap억(self, refresh: bool) -> float:
456
+ """
457
+ 시가총액(억원) 반환
458
+ :return:
459
+ """
460
+ c101r = self.c101.get_recent(refresh)
461
+ 시가총액 = int(utils.to_int(c101r.get('시가총액', math.nan)) / 100000000)
462
+ eval_logger.debug(f"시가총액: {시가총액}억원")
463
+ return 시가총액
464
+
465
+ def _calc주주수익률(self, 시가총액_억: float, refresh: bool) -> Tuple[str, float, float]:
466
+ self.c103.page = 'c103현금흐름표q'
467
+ d, 재무활동현금흐름 = self.c103.sum_recent_4q('재무활동으로인한현금흐름', refresh)
468
+ try:
469
+ 주주수익률 = round((재무활동현금흐름 / 시가총액_억 * -100), 2)
470
+ except ZeroDivisionError:
471
+ 주주수익률 = math.nan
472
+ eval_logger.warning(f'{self} 주주수익률: {주주수익률} 재무활동현금흐름: {재무활동현금흐름}')
473
+ return d, 주주수익률, 재무활동현금흐름
474
+
475
+ def _calc이익지표(self, 시가총액_억: float, refresh: bool) -> Tuple[str, float, float, float]:
476
+ d1, 지배주주당기순이익 = Tools.calc당기순이익(self.c103, refresh)
477
+ self.c103.page = 'c103현금흐름표q'
478
+ d2, 영업활동현금흐름 = self.c103.sum_recent_4q('영업활동으로인한현금흐름', refresh)
479
+ try:
480
+ 이익지표 = round(((지배주주당기순이익 - 영업활동현금흐름) / 시가총액_억) * 100, 2)
481
+ except ZeroDivisionError:
482
+ 이익지표 = math.nan
483
+ eval_logger.warning(f'{self} 이익지표: {이익지표} 영업활동현금흐름: {영업활동현금흐름} 지배주주당기순이익: {지배주주당기순이익}')
484
+ try:
485
+ date, *_ = Tools.date_set(d1, d2)
486
+ except ValueError:
487
+ # 날짜 데이터가 없는경우
488
+ date = ''
489
+ return date , 이익지표, 영업활동현금흐름, 지배주주당기순이익
490
+
491
+ def _calc투자수익률(self, refresh: bool) -> tuple:
492
+ self.c104.page = 'c104q'
493
+ self.c106.page = 'c106q'
494
+ d1, roic_r = self.c104.sum_recent_4q('ROIC', refresh)
495
+ _, roic_dict = self.c104.find('ROIC', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
496
+ d2, roe_r = self.c104.latest_value_pop2('ROE', refresh)
497
+ roe106 = self.c106.find('ROE', refresh)
498
+ d3, roa_r = self.c104.latest_value_pop2('ROA', refresh)
499
+
500
+ try:
501
+ date, *_ = Tools.date_set(d1, d2, d3)
502
+ except ValueError:
503
+ # 날짜 데이터가 없는경우
504
+ date = ''
505
+
506
+ return date, roic_r, roic_dict, roe_r, roe106, roa_r
507
+
508
+ def _calcFCF(self, refresh: bool) -> dict:
509
+ """
510
+ FCF 계산
511
+ Returns:
512
+ dict: 계산된 fcf 딕셔너리 또는 영업현금흐름 없는 경우 - {}
513
+
514
+ Note:
515
+ CAPEX 가 없는 업종은 영업활동현금흐름을 그대로 사용한다.\n
516
+
517
+ """
518
+ self.c103.page = 'c103현금흐름표y'
519
+ _, 영업활동현금흐름_dict = self.c103.find('영업활동으로인한현금흐름', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
520
+
521
+ self.c103.page = 'c103재무상태표y'
522
+ _, capex = self.c103.find('*CAPEX', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
523
+
524
+ eval_logger.debug(f'영업활동현금흐름 {영업활동현금흐름_dict}')
525
+ eval_logger.debug(f'CAPEX {capex}')
526
+
527
+ if len(영업활동현금흐름_dict) == 0:
528
+ return {}
529
+
530
+ if len(capex) == 0:
531
+ # CAPEX 가 없는 업종은 영업활동현금흐름을 그대로 사용한다.
532
+ eval_logger.warning(f"{self} - CAPEX가 없는 업종으로 영업현금흐름을 그대로 사용합니다..")
533
+ return 영업활동현금흐름_dict
534
+
535
+ # 영업 활동으로 인한 현금 흐름에서 CAPEX 를 각 연도별로 빼주어 fcf 를 구하고 리턴값으로 fcf 딕셔너리를 반환한다.
536
+ fcf_dict = {}
537
+ for i in range(len(영업활동현금흐름_dict)):
538
+ # 영업활동현금흐름에서 아이템을 하나씩 꺼내서 CAPEX 전체와 비교하여 같으면 차를 구해서 fcf_dict 에 추가한다.
539
+ 영업활동현금흐름date, 영업활동현금흐름value = 영업활동현금흐름_dict.popitem()
540
+ # 해당 연도의 capex 가 없는 경우도 있어 일단 capex를 0으로 치고 먼저 추가한다.
541
+ fcf_dict[영업활동현금흐름date] = 영업활동현금흐름value
542
+ for CAPEXdate, CAPEXvalue in capex.items():
543
+ if 영업활동현금흐름date == CAPEXdate:
544
+ fcf_dict[영업활동현금흐름date] = round(영업활동현금흐름value - CAPEXvalue, 2)
545
+
546
+ eval_logger.debug(f'fcf_dict {fcf_dict}')
547
+ # 연도순으로 정렬해서 딕셔너리로 반환한다.
548
+ return dict(sorted(fcf_dict.items(), reverse=False))
549
+
550
+ def _calcPFCF(self, 시가총액_억: float, fcf_dict: dict) -> dict:
551
+ """Price to Free Cash Flow Ratio(주가 대비 자유 현금 흐름 비율)계산
552
+
553
+ PFCF = 시가총액 / FCF
554
+
555
+ Note:
556
+ https://www.investopedia.com/terms/p/pricetofreecashflow.asp
557
+ """
558
+ if math.isnan(시가총액_억):
559
+ eval_logger.warning(f"{self} - 시가총액이 nan으로 pFCF를 계산할수 없습니다.")
560
+ return {}
561
+
562
+ # pfcf 계산
563
+ pfcf_dict = {}
564
+ for FCFdate, FCFvalue in fcf_dict.items():
565
+ if FCFvalue == 0:
566
+ pfcf_dict[FCFdate] = math.nan
567
+ else:
568
+ pfcf_dict[FCFdate] = round(시가총액_억 / FCFvalue, 2)
569
+
570
+ pfcf_dict = mymongo.C1034.del_unnamed_key(pfcf_dict)
571
+
572
+ eval_logger.debug(f'pfcf_dict : {pfcf_dict}')
573
+ return pfcf_dict
574
+
575
+ def _calc가치지표(self, 시가총액_억: float, refresh: bool) -> tuple:
576
+ self.c104.page = 'c104q'
577
+
578
+ fcf_dict = self._calcFCF(refresh)
579
+ pfcf_dict = self._calcPFCF(시가총액_억, fcf_dict)
580
+
581
+ d, pcr_dict = self.c104.find('PCR', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
582
+ return d, fcf_dict, pfcf_dict, pcr_dict
583
+
584
+ def _score(self) -> list:
585
+ return [0,]
586
+
587
+ def _generate_data(self, refresh: bool) -> MilData:
588
+ eval_logger.info(f"In generate_data..refresh : {refresh}")
589
+ 시가총액_억 = self.get_marketcap억(refresh)
590
+ eval_logger.info(f"{self} 시가총액(억) : {시가총액_억}")
591
+
592
+ d1, 주주수익률, 재무활동현금흐름 = self._calc주주수익률(시가총액_억, refresh)
593
+ eval_logger.info(f"{self} 주주수익률 : {주주수익률}, {d1}")
594
+
595
+ d2, 이익지표, 영업활동현금흐름, 지배주주당기순이익 = self._calc이익지표(시가총액_억, refresh)
596
+ eval_logger.info(f"{self} 이익지표 : {이익지표}, {d2}")
597
+
598
+ d3, roic_r, roic_dict, roe_r, roe106, roa_r = self._calc투자수익률(refresh)
599
+ d4, fcf_dict, pfcf_dict, pcr_dict = self._calc가치지표(시가총액_억, refresh)
600
+
601
+ score = self._score()
602
+
603
+ try:
604
+ date_list = Tools.date_set(d1, d2, d3, d4)
605
+ except ValueError:
606
+ # 날짜 데이터가 없는경우
607
+ date_list = ['',]
608
+
609
+ return MilData(
610
+ code= self.code,
611
+ name= self.name,
612
+
613
+ 시가총액억= 시가총액_억,
614
+
615
+ 주주수익률= 주주수익률,
616
+ 재무활동현금흐름= 재무활동현금흐름,
617
+
618
+ 이익지표= 이익지표,
619
+ 영업활동현금흐름= 영업활동현금흐름,
620
+ 지배주주당기순이익= 지배주주당기순이익,
621
+
622
+ roic_r= roic_r,
623
+ roic_dict= roic_dict,
624
+ roe_r= roe_r,
625
+ roe_106= roe106,
626
+ roa_r= roa_r,
627
+
628
+ fcf_dict= fcf_dict,
629
+ pfcf_dict= pfcf_dict,
630
+ pcr_dict= pcr_dict,
631
+
632
+ score= score,
633
+ date = date_list,
634
+ )
635
+
636
+ def get(self, refresh = False, verbose = True) -> MilData:
637
+ """
638
+ MilData 형식의 데이터를 계산하여 리턴하고 레디스 캐시에 저장한다.
639
+ :param refresh:
640
+ :return:
641
+ """
642
+ redis_name = f"{self.code}_mil"
643
+ eval_logger.info(f"{self} MilData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
644
+ if verbose:
645
+ print(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
646
+
647
+ def fetch_generate_data(refresh_in: bool) -> dict:
648
+ return asdict(self._generate_data(refresh_in))
649
+
650
+ return MilData(**myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_data, refresh, timer=expire_time))
651
+
652
+
653
+ @dataclass()
654
+ class BlueData:
655
+ code: str
656
+ name: str
657
+
658
+ 유동비율: float
659
+
660
+ 이자보상배율_r: float
661
+ 이자보상배율_dict: dict
662
+
663
+ 순운전자본회전율_r: float
664
+ 순운전자본회전율_dict: dict
665
+
666
+ 재고자산회전율_r: float
667
+ 재고자산회전율_dict: dict
668
+ 재고자산회전율_c106: dict
669
+
670
+ 순부채비율_r: float
671
+ 순부채비율_dict: dict
672
+
673
+ score: list
674
+ date: list
675
+
676
+
677
+ class Blue:
678
+ def __init__(self, code: str):
679
+ assert utils.is_6digit(code), f'Invalid value : {code}'
680
+ eval_logger.debug(f"Blue : 종목코드 ({code})")
681
+
682
+ self.c101 = myredis.C101(code)
683
+ self.c103 = myredis.C103(code, 'c103재무상태표q')
684
+ self.c104 = myredis.C104(code, 'c104q')
685
+
686
+ self.name = self.c101.get_name()
687
+ self._code = code
688
+
689
+ def __str__(self):
690
+ return f"Blue({self.code}/{self.name})"
691
+
692
+ @property
693
+ def code(self) -> str:
694
+ return self._code
695
+
696
+ @code.setter
697
+ def code(self, code: str):
698
+ assert utils.is_6digit(code), f'Invalid value : {code}'
699
+ eval_logger.debug(f"Blue : 종목코드 변경({self.code} -> {code})")
700
+
701
+ self.c101.code = code
702
+ self.c103.code = code
703
+ self.c104.code = code
704
+
705
+ self.name = self.c101.get_name()
706
+ self._code = code
707
+
708
+ def _calc유동비율(self, pop_count: int, refresh: bool) -> Tuple[str, float]:
709
+ """유동비율계산 - Blue에서 사용
710
+
711
+ c104q에서 최근유동비율 찾아보고 유효하지 않거나 \n
712
+ 100이하인 경우에는수동으로 계산해서 다시 한번 평가해 본다.\n
713
+ """
714
+ eval_logger.info(f'In the calc유동비율... refresh : {refresh}')
715
+ self.c104.page = 'c104q'
716
+
717
+ 유동비율date, 유동비율value = self.c104.latest_value('유동비율', pop_count=pop_count)
718
+ eval_logger.info(f'{self} 유동비율 : {유동비율value}/({유동비율date})')
719
+
720
+ if math.isnan(유동비율value) or 유동비율value < 100:
721
+ 유동자산date, 유동자산value = Tools.calc유동자산(self.c103, refresh)
722
+ 유동부채date, 유동부채value = Tools.calc유동부채(self.c103, refresh)
723
+
724
+ self.c103.page = 'c103현금흐름표q'
725
+ 추정영업현금흐름date, 추정영업현금흐름value = self.c103.sum_recent_4q('영업활동으로인한현금흐름', refresh)
726
+ eval_logger.debug(f'{self} 계산전 유동비율 : {유동비율value} / ({유동비율date})')
727
+
728
+ 계산된유동비율 = 0
729
+ try:
730
+ 계산된유동비율 = round(((유동자산value + 추정영업현금흐름value) / 유동부채value) * 100, 2)
731
+ except ZeroDivisionError:
732
+ eval_logger.info(f'유동자산: {유동자산value} + 추정영업현금흐름: {추정영업현금흐름value} / 유동부채: {유동부채value}')
733
+ 계산된유동비율 = float('inf')
734
+ finally:
735
+ eval_logger.debug(f'{self} 계산된 유동비율 : {계산된유동비율}')
736
+
737
+ try:
738
+ date, *_ = Tools.date_set(유동자산date, 유동부채date, 추정영업현금흐름date)
739
+ except ValueError:
740
+ # 날짜 데이터가 없는경우
741
+ date = ''
742
+ eval_logger.warning(f'{self} 유동비율 이상(100 이하 또는 nan) : {유동비율value} -> 재계산 : {계산된유동비율}')
743
+ return date, 계산된유동비율
744
+ else:
745
+ return 유동비율date, 유동비율value
746
+
747
+ def _score(self) -> list:
748
+ return [0,]
749
+
750
+ def _generate_data(self, refresh: bool) -> BlueData:
751
+ d1, 유동비율 = self._calc유동비율(pop_count=3, refresh=refresh)
752
+ eval_logger.info(f'유동비율 {유동비율} / [{d1}]')
753
+
754
+ 재고자산회전율_c106 = myredis.C106.make_like_c106(self.code, 'c104q', '재고자산회전율', refresh)
755
+
756
+ self.c104.page = 'c104y'
757
+ _, 이자보상배율_dict = self.c104.find('이자보상배율', remove_yoy=True, refresh=refresh)
758
+ _, 순운전자본회전율_dict = self.c104.find('순운전자본회전율', remove_yoy=True, refresh=refresh)
759
+ _, 재고자산회전율_dict = self.c104.find('재고자산회전율', remove_yoy=True, refresh=refresh)
760
+ _, 순부채비율_dict = self.c104.find('순부채비율', remove_yoy=True, refresh=refresh)
761
+
762
+ self.c104.page = 'c104q'
763
+ d6, 이자보상배율_r = self.c104.latest_value_pop2('이자보상배율', refresh)
764
+ d7, 순운전자본회전율_r = self.c104.latest_value_pop2('순운전자본회전율', refresh)
765
+ d8, 재고자산회전율_r = self.c104.latest_value_pop2('재고자산회전율', refresh)
766
+ d9, 순부채비율_r = self.c104.latest_value_pop2('순부채비율', refresh)
767
+
768
+ if len(이자보상배율_dict) == 0:
769
+ eval_logger.warning(f'empty dict - 이자보상배율 : {이자보상배율_r} / {이자보상배율_dict}')
770
+
771
+ if len(순운전자본회전율_dict) == 0:
772
+ eval_logger.warning(f'empty dict - 순운전자본회전율 : {순운전자본회전율_r} / {순운전자본회전율_dict}')
773
+
774
+ if len(재고자산회전율_dict) == 0:
775
+ eval_logger.warning(f'empty dict - 재고자산회전율 : {재고자산회전율_r} / {재고자산회전율_dict}')
776
+
777
+ if len(순부채비율_dict) == 0:
778
+ eval_logger.warning(f'empty dict - 순부채비율 : {순부채비율_r} / {순부채비율_dict}')
779
+
780
+ score = self._score()
781
+
782
+ try:
783
+ date_list = Tools.date_set(d1, d6, d7, d8, d9)
784
+ except ValueError:
785
+ # 날짜 데이터가 없는경우
786
+ date_list = ['',]
787
+
788
+ return BlueData(
789
+ code= self.code,
790
+ name= self.name,
791
+ 유동비율= 유동비율,
792
+ 이자보상배율_r= 이자보상배율_r,
793
+ 이자보상배율_dict= 이자보상배율_dict,
794
+
795
+ 순운전자본회전율_r= 순운전자본회전율_r,
796
+ 순운전자본회전율_dict= 순운전자본회전율_dict,
797
+
798
+ 재고자산회전율_r= 재고자산회전율_r,
799
+ 재고자산회전율_dict= 재고자산회전율_dict,
800
+ 재고자산회전율_c106= 재고자산회전율_c106,
801
+
802
+ 순부채비율_r= 순부채비율_r,
803
+ 순부채비율_dict= 순부채비율_dict,
804
+
805
+ score= score,
806
+ date= date_list,
807
+ )
808
+
809
+ def get(self, refresh = False, verbose = True) -> BlueData:
810
+ """
811
+ BlueData 형식의 데이터를 계산하여 리턴하고 레디스 캐시에 저장한다.
812
+ :param refresh:
813
+ :return:
814
+ """
815
+ redis_name = f"{self.code}_blue"
816
+ eval_logger.info(f"{self} BlueData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
817
+ if verbose:
818
+ print(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
819
+
820
+ def fetch_generate_data(refresh_in: bool) -> dict:
821
+ return asdict(self._generate_data(refresh_in))
822
+
823
+ return BlueData(**myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_data, refresh, timer=expire_time))
824
+
825
+
826
+
827
+ @dataclass()
828
+ class GrowthData:
829
+ code: str
830
+ name: str
831
+
832
+ 매출액증가율_r: float
833
+ 매출액증가율_dict: dict
834
+
835
+ 영업이익률_c106: dict
836
+
837
+ score: list
838
+ date: list
839
+
840
+
841
+ class Growth:
842
+ def __init__(self, code: str):
843
+ assert utils.is_6digit(code), f'Invalid value : {code}'
844
+ eval_logger.debug(f"Growth : 종목코드 ({code})")
845
+
846
+ self.c101 = myredis.C101(code)
847
+ self.c104 = myredis.C104(code, 'c104q')
848
+ self.c106 = myredis.C106(code, 'c106q')
849
+
850
+ self.name = self.c101.get_name()
851
+ self._code = code
852
+
853
+ def __str__(self):
854
+ return f"Growth({self.code}/{self.name})"
855
+
856
+ @property
857
+ def code(self) -> str:
858
+ return self._code
859
+
860
+ @code.setter
861
+ def code(self, code: str):
862
+ assert utils.is_6digit(code), f'Invalid value : {code}'
863
+ eval_logger.debug(f"Growth : 종목코드 변경({self.code} -> {code})")
864
+
865
+ self.c101.code = code
866
+ self.c104.code = code
867
+ self.c106.code = code
868
+
869
+ self.name = self.c101.get_name()
870
+ self._code = code
871
+
872
+ def _score(self) -> list:
873
+ return [0,]
874
+
875
+ def _generate_data(self, refresh=False) -> GrowthData:
876
+ self.c104.page = 'c104y'
877
+ _, 매출액증가율_dict = self.c104.find('매출액증가율', remove_yoy=True, refresh=refresh)
878
+
879
+ self.c104.page = 'c104q'
880
+ d2, 매출액증가율_r = self.c104.latest_value_pop2('매출액증가율')
881
+
882
+ eval_logger.info(f'매출액증가율 : {매출액증가율_r} {매출액증가율_dict}')
883
+
884
+ # c106 에서 타 기업과 영업이익률 비교
885
+ self.c106.page = 'c106y'
886
+ 영업이익률_c106 = self.c106.find('영업이익률', refresh)
887
+
888
+ score = self._score()
889
+
890
+ try:
891
+ date_list = Tools.date_set(d2)
892
+ except ValueError:
893
+ # 날짜 데이터가 없는경우
894
+ date_list = ['', ]
895
+
896
+ return GrowthData(
897
+ code= self.code,
898
+ name= self.name,
899
+
900
+ 매출액증가율_r= 매출액증가율_r,
901
+ 매출액증가율_dict= 매출액증가율_dict,
902
+
903
+ 영업이익률_c106= 영업이익률_c106,
904
+
905
+ score= score,
906
+ date= date_list,
907
+ )
908
+
909
+ def get(self, refresh = False, verbose = True) -> GrowthData:
910
+ """
911
+ GrowthData 형식의 데이터를 계산하여 리턴하고 레디스 캐시에 저장한다.
912
+ :param refresh:
913
+ :return:
914
+ """
915
+ redis_name = f"{self.code}_growth"
916
+ eval_logger.info(f"{self} GrowthData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
917
+ if verbose:
918
+ print(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
919
+
920
+ def fetch_generate_data(refresh_in: bool) -> dict:
921
+ return asdict(self._generate_data(refresh_in))
922
+
923
+ return GrowthData(**myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_data, refresh, timer=expire_time))
924
+
925
+
926
+
927
+
928
+ """
929
+ - 각분기의 합이 연이 아닌 타이틀(즉 sum_4q를 사용하면 안됨)
930
+ '*(지배)당기순이익'
931
+ '*(비지배)당기순이익'
932
+ '장기차입금'
933
+ '현금및예치금'
934
+ '매도가능금융자산'
935
+ '매도파생결합증권'
936
+ '만기보유금융자산'
937
+ '당기손익-공정가치측정금융부채'
938
+ '당기손익인식(지정)금융부채'
939
+ '단기매매금융자산'
940
+ '단기매매금융부채'
941
+ '예수부채'
942
+ '차입부채'
943
+ '기타부채'
944
+ '보험계약부채(책임준비금)'
945
+ '*CAPEX'
946
+ 'ROE'
947
+ """
948
+
949
+ """
950
+ - sum_4q를 사용해도 되는 타이틀
951
+ '자산총계'
952
+ '당기순이익'
953
+ '유동자산'
954
+ '유동부채'
955
+ '비유동부채'
956
+
957
+ '영업활동으로인한현금흐름'
958
+ '재무활동으로인한현금흐름'
959
+ 'ROIC'
960
+ """