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