analyser_hj3415 2.10.5__py3-none-any.whl → 3.0.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
analyser_hj3415/eval.py DELETED
@@ -1,960 +0,0 @@
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 .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 * 48
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
- """