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/__init__.py +13 -0
- analyser_hj3415/analyser/__init__.py +0 -0
- analyser_hj3415/analyser/eval/__init__.py +4 -0
- analyser_hj3415/analyser/eval/blue.py +187 -0
- analyser_hj3415/analyser/eval/common.py +267 -0
- analyser_hj3415/analyser/eval/growth.py +110 -0
- analyser_hj3415/analyser/eval/mil.py +274 -0
- analyser_hj3415/analyser/eval/red.py +295 -0
- analyser_hj3415/{score.py → analyser/score.py} +25 -24
- analyser_hj3415/analyser/tsa/__init__.py +2 -0
- analyser_hj3415/analyser/tsa/lstm.py +670 -0
- analyser_hj3415/analyser/tsa/prophet.py +207 -0
- analyser_hj3415/cli.py +11 -88
- {analyser_hj3415-2.10.5.dist-info → analyser_hj3415-3.0.0.dist-info}/METADATA +3 -3
- analyser_hj3415-3.0.0.dist-info/RECORD +22 -0
- analyser_hj3415/eval.py +0 -960
- analyser_hj3415/tsa.py +0 -708
- analyser_hj3415-2.10.5.dist-info/RECORD +0 -14
- {analyser_hj3415-2.10.5.dist-info → analyser_hj3415-3.0.0.dist-info}/WHEEL +0 -0
- {analyser_hj3415-2.10.5.dist-info → analyser_hj3415-3.0.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,274 @@
|
|
1
|
+
import os
|
2
|
+
from dataclasses import dataclass, asdict
|
3
|
+
from typing import Tuple
|
4
|
+
import math
|
5
|
+
|
6
|
+
from utils_hj3415 import tools, setup_logger
|
7
|
+
from db_hj3415 import myredis, mymongo
|
8
|
+
|
9
|
+
from analyser_hj3415.analyser.eval.common import Tools
|
10
|
+
|
11
|
+
|
12
|
+
mylogger = setup_logger(__name__,'WARNING')
|
13
|
+
expire_time = tools.to_int(os.getenv('DEFAULT_EXPIRE_TIME_H', 48)) * 3600
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class MilData:
|
18
|
+
code: str
|
19
|
+
name: str
|
20
|
+
|
21
|
+
시가총액억: float
|
22
|
+
|
23
|
+
주주수익률: float
|
24
|
+
재무활동현금흐름: float
|
25
|
+
|
26
|
+
이익지표: float
|
27
|
+
영업활동현금흐름: float
|
28
|
+
지배주주당기순이익: float
|
29
|
+
|
30
|
+
#투자수익률
|
31
|
+
roic_r: float
|
32
|
+
roic_dict: dict
|
33
|
+
roe_r: float
|
34
|
+
roe_106: dict
|
35
|
+
roa_r: float
|
36
|
+
|
37
|
+
#가치지표
|
38
|
+
fcf_dict: dict
|
39
|
+
pfcf_dict: dict
|
40
|
+
pcr_dict: dict
|
41
|
+
|
42
|
+
score: list
|
43
|
+
date: list
|
44
|
+
|
45
|
+
|
46
|
+
class Mil:
|
47
|
+
def __init__(self, code: str):
|
48
|
+
assert tools.is_6digit(code), f'Invalid value : {code}'
|
49
|
+
mylogger.debug(f"Mil : 종목코드 ({code})")
|
50
|
+
|
51
|
+
self.c101 = myredis.C101(code)
|
52
|
+
self.c103 = myredis.C103(code, 'c103현금흐름표q')
|
53
|
+
self.c104 = myredis.C104(code, 'c104q')
|
54
|
+
self.c106 = myredis.C106(code, 'c106q')
|
55
|
+
|
56
|
+
self.name = self.c101.get_name()
|
57
|
+
self._code = code
|
58
|
+
|
59
|
+
def __str__(self):
|
60
|
+
return f"Mil({self.code}/{self.name})"
|
61
|
+
|
62
|
+
@property
|
63
|
+
def code(self) -> str:
|
64
|
+
return self._code
|
65
|
+
|
66
|
+
@code.setter
|
67
|
+
def code(self, code: str):
|
68
|
+
assert tools.is_6digit(code), f'Invalid value : {code}'
|
69
|
+
mylogger.debug(f"Mil : 종목코드 변경({self.code} -> {code})")
|
70
|
+
|
71
|
+
self.c101.code = code
|
72
|
+
self.c103.code = code
|
73
|
+
self.c104.code = code
|
74
|
+
self.c106.code = code
|
75
|
+
|
76
|
+
self.name = self.c101.get_name()
|
77
|
+
self._code = code
|
78
|
+
|
79
|
+
def get_marketcap억(self, refresh: bool) -> float:
|
80
|
+
"""
|
81
|
+
시가총액(억원) 반환
|
82
|
+
:return:
|
83
|
+
"""
|
84
|
+
c101r = self.c101.get_recent(refresh)
|
85
|
+
시가총액 = tools.to_int(tools.to_float(c101r.get('시가총액', math.nan)) / 100000000)
|
86
|
+
mylogger.debug(f"시가총액: {시가총액}억원")
|
87
|
+
return 시가총액
|
88
|
+
|
89
|
+
def _calc주주수익률(self, 시가총액_억: float, refresh: bool) -> Tuple[str, float, float]:
|
90
|
+
self.c103.page = 'c103현금흐름표q'
|
91
|
+
d, 재무활동현금흐름 = self.c103.sum_recent_4q('재무활동으로인한현금흐름', refresh)
|
92
|
+
try:
|
93
|
+
주주수익률 = round((재무활동현금흐름 / 시가총액_억 * -100), 2)
|
94
|
+
except ZeroDivisionError:
|
95
|
+
주주수익률 = math.nan
|
96
|
+
mylogger.warning(f'{self} 주주수익률: {주주수익률} 재무활동현금흐름: {재무활동현금흐름}')
|
97
|
+
return d, 주주수익률, 재무활동현금흐름
|
98
|
+
|
99
|
+
def _calc이익지표(self, 시가총액_억: float, refresh: bool) -> Tuple[str, float, float, float]:
|
100
|
+
d1, 지배주주당기순이익 = Tools.calc당기순이익(self.c103, refresh)
|
101
|
+
self.c103.page = 'c103현금흐름표q'
|
102
|
+
d2, 영업활동현금흐름 = self.c103.sum_recent_4q('영업활동으로인한현금흐름', refresh)
|
103
|
+
try:
|
104
|
+
이익지표 = round(((지배주주당기순이익 - 영업활동현금흐름) / 시가총액_억) * 100, 2)
|
105
|
+
except ZeroDivisionError:
|
106
|
+
이익지표 = math.nan
|
107
|
+
mylogger.warning(f'{self} 이익지표: {이익지표} 영업활동현금흐름: {영업활동현금흐름} 지배주주당기순이익: {지배주주당기순이익}')
|
108
|
+
try:
|
109
|
+
date, *_ = Tools.date_set(d1, d2)
|
110
|
+
except ValueError:
|
111
|
+
# 날짜 데이터가 없는경우
|
112
|
+
date = ''
|
113
|
+
return date , 이익지표, 영업활동현금흐름, 지배주주당기순이익
|
114
|
+
|
115
|
+
def _calc투자수익률(self, refresh: bool) -> tuple:
|
116
|
+
self.c104.page = 'c104q'
|
117
|
+
self.c106.page = 'c106q'
|
118
|
+
d1, roic_r = self.c104.sum_recent_4q('ROIC', refresh)
|
119
|
+
_, roic_dict = self.c104.find('ROIC', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
|
120
|
+
d2, roe_r = self.c104.latest_value_pop2('ROE', refresh)
|
121
|
+
roe106 = self.c106.find('ROE', refresh)
|
122
|
+
d3, roa_r = self.c104.latest_value_pop2('ROA', refresh)
|
123
|
+
|
124
|
+
try:
|
125
|
+
date, *_ = Tools.date_set(d1, d2, d3)
|
126
|
+
except ValueError:
|
127
|
+
# 날짜 데이터가 없는경우
|
128
|
+
date = ''
|
129
|
+
|
130
|
+
return date, roic_r, roic_dict, roe_r, roe106, roa_r
|
131
|
+
|
132
|
+
def _calcFCF(self, refresh: bool) -> dict:
|
133
|
+
"""
|
134
|
+
FCF 계산
|
135
|
+
Returns:
|
136
|
+
dict: 계산된 fcf 딕셔너리 또는 영업현금흐름 없는 경우 - {}
|
137
|
+
|
138
|
+
Note:
|
139
|
+
CAPEX 가 없는 업종은 영업활동현금흐름을 그대로 사용한다.\n
|
140
|
+
|
141
|
+
"""
|
142
|
+
self.c103.page = 'c103현금흐름표y'
|
143
|
+
_, 영업활동현금흐름_dict = self.c103.find('영업활동으로인한현금흐름', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
|
144
|
+
|
145
|
+
self.c103.page = 'c103재무상태표y'
|
146
|
+
_, capex = self.c103.find('*CAPEX', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
|
147
|
+
|
148
|
+
mylogger.debug(f'영업활동현금흐름 {영업활동현금흐름_dict}')
|
149
|
+
mylogger.debug(f'CAPEX {capex}')
|
150
|
+
|
151
|
+
if len(영업활동현금흐름_dict) == 0:
|
152
|
+
return {}
|
153
|
+
|
154
|
+
if len(capex) == 0:
|
155
|
+
# CAPEX 가 없는 업종은 영업활동현금흐름을 그대로 사용한다.
|
156
|
+
mylogger.warning(f"{self} - CAPEX가 없는 업종으로 영업현금흐름을 그대로 사용합니다..")
|
157
|
+
return 영업활동현금흐름_dict
|
158
|
+
|
159
|
+
# 영업 활동으로 인한 현금 흐름에서 CAPEX 를 각 연도별로 빼주어 fcf 를 구하고 리턴값으로 fcf 딕셔너리를 반환한다.
|
160
|
+
fcf_dict = {}
|
161
|
+
for i in range(len(영업활동현금흐름_dict)):
|
162
|
+
# 영업활동현금흐름에서 아이템을 하나씩 꺼내서 CAPEX 전체와 비교하여 같으면 차를 구해서 fcf_dict 에 추가한다.
|
163
|
+
영업활동현금흐름date, 영업활동현금흐름value = 영업활동현금흐름_dict.popitem()
|
164
|
+
# 해당 연도의 capex 가 없는 경우도 있어 일단 capex를 0으로 치고 먼저 추가한다.
|
165
|
+
fcf_dict[영업활동현금흐름date] = 영업활동현금흐름value
|
166
|
+
for CAPEXdate, CAPEXvalue in capex.items():
|
167
|
+
if 영업활동현금흐름date == CAPEXdate:
|
168
|
+
fcf_dict[영업활동현금흐름date] = round(영업활동현금흐름value - CAPEXvalue, 2)
|
169
|
+
|
170
|
+
mylogger.debug(f'fcf_dict {fcf_dict}')
|
171
|
+
# 연도순으로 정렬해서 딕셔너리로 반환한다.
|
172
|
+
return dict(sorted(fcf_dict.items(), reverse=False))
|
173
|
+
|
174
|
+
def _calcPFCF(self, 시가총액_억: float, fcf_dict: dict) -> dict:
|
175
|
+
"""Price to Free Cash Flow Ratio(주가 대비 자유 현금 흐름 비율)계산
|
176
|
+
|
177
|
+
PFCF = 시가총액 / FCF
|
178
|
+
|
179
|
+
Note:
|
180
|
+
https://www.investopedia.com/terms/p/pricetofreecashflow.asp
|
181
|
+
"""
|
182
|
+
if math.isnan(시가총액_억):
|
183
|
+
mylogger.warning(f"{self} - 시가총액이 nan으로 pFCF를 계산할수 없습니다.")
|
184
|
+
return {}
|
185
|
+
|
186
|
+
# pfcf 계산
|
187
|
+
pfcf_dict = {}
|
188
|
+
for FCFdate, FCFvalue in fcf_dict.items():
|
189
|
+
if FCFvalue == 0:
|
190
|
+
pfcf_dict[FCFdate] = math.nan
|
191
|
+
else:
|
192
|
+
pfcf_dict[FCFdate] = round(시가총액_억 / FCFvalue, 2)
|
193
|
+
|
194
|
+
pfcf_dict = mymongo.C1034.del_unnamed_key(pfcf_dict)
|
195
|
+
|
196
|
+
mylogger.debug(f'pfcf_dict : {pfcf_dict}')
|
197
|
+
return pfcf_dict
|
198
|
+
|
199
|
+
def _calc가치지표(self, 시가총액_억: float, refresh: bool) -> tuple:
|
200
|
+
self.c104.page = 'c104q'
|
201
|
+
|
202
|
+
fcf_dict = self._calcFCF(refresh)
|
203
|
+
pfcf_dict = self._calcPFCF(시가총액_억, fcf_dict)
|
204
|
+
|
205
|
+
d, pcr_dict = self.c104.find('PCR', remove_yoy=True, del_unnamed_key=True, refresh=refresh)
|
206
|
+
return d, fcf_dict, pfcf_dict, pcr_dict
|
207
|
+
|
208
|
+
def _score(self) -> list:
|
209
|
+
return [0,]
|
210
|
+
|
211
|
+
def _generate_data(self, refresh: bool) -> MilData:
|
212
|
+
mylogger.info(f"In generate_data..refresh : {refresh}")
|
213
|
+
시가총액_억 = self.get_marketcap억(refresh)
|
214
|
+
mylogger.info(f"{self} 시가총액(억) : {시가총액_억}")
|
215
|
+
|
216
|
+
d1, 주주수익률, 재무활동현금흐름 = self._calc주주수익률(시가총액_억, refresh)
|
217
|
+
mylogger.info(f"{self} 주주수익률 : {주주수익률}, {d1}")
|
218
|
+
|
219
|
+
d2, 이익지표, 영업활동현금흐름, 지배주주당기순이익 = self._calc이익지표(시가총액_억, refresh)
|
220
|
+
mylogger.info(f"{self} 이익지표 : {이익지표}, {d2}")
|
221
|
+
|
222
|
+
d3, roic_r, roic_dict, roe_r, roe106, roa_r = self._calc투자수익률(refresh)
|
223
|
+
d4, fcf_dict, pfcf_dict, pcr_dict = self._calc가치지표(시가총액_억, refresh)
|
224
|
+
|
225
|
+
score = self._score()
|
226
|
+
|
227
|
+
try:
|
228
|
+
date_list = Tools.date_set(d1, d2, d3, d4)
|
229
|
+
except ValueError:
|
230
|
+
# 날짜 데이터가 없는경우
|
231
|
+
date_list = ['',]
|
232
|
+
|
233
|
+
return MilData(
|
234
|
+
code= self.code,
|
235
|
+
name= self.name,
|
236
|
+
|
237
|
+
시가총액억= 시가총액_억,
|
238
|
+
|
239
|
+
주주수익률= 주주수익률,
|
240
|
+
재무활동현금흐름= 재무활동현금흐름,
|
241
|
+
|
242
|
+
이익지표= 이익지표,
|
243
|
+
영업활동현금흐름= 영업활동현금흐름,
|
244
|
+
지배주주당기순이익= 지배주주당기순이익,
|
245
|
+
|
246
|
+
roic_r= roic_r,
|
247
|
+
roic_dict= roic_dict,
|
248
|
+
roe_r= roe_r,
|
249
|
+
roe_106= roe106,
|
250
|
+
roa_r= roa_r,
|
251
|
+
|
252
|
+
fcf_dict= fcf_dict,
|
253
|
+
pfcf_dict= pfcf_dict,
|
254
|
+
pcr_dict= pcr_dict,
|
255
|
+
|
256
|
+
score= score,
|
257
|
+
date = date_list,
|
258
|
+
)
|
259
|
+
|
260
|
+
def get(self, refresh = False, verbose = True) -> MilData:
|
261
|
+
"""
|
262
|
+
MilData 형식의 데이터를 계산하여 리턴하고 레디스 캐시에 저장한다.
|
263
|
+
:param refresh:
|
264
|
+
:return:
|
265
|
+
"""
|
266
|
+
redis_name = f"{self.code}_mil"
|
267
|
+
mylogger.info(f"{self} MilData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
|
268
|
+
if verbose:
|
269
|
+
print(f"{self} redisname: '{redis_name}' / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
270
|
+
|
271
|
+
def fetch_generate_data(refresh_in: bool) -> dict:
|
272
|
+
return asdict(self._generate_data(refresh_in))
|
273
|
+
|
274
|
+
return MilData(**myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_data, refresh, timer=expire_time))
|
@@ -0,0 +1,295 @@
|
|
1
|
+
import os
|
2
|
+
from dataclasses import dataclass, asdict
|
3
|
+
from collections import OrderedDict
|
4
|
+
from typing import Tuple
|
5
|
+
import math
|
6
|
+
|
7
|
+
from utils_hj3415 import tools, setup_logger
|
8
|
+
from db_hj3415 import myredis
|
9
|
+
|
10
|
+
from analyser_hj3415.analyser.eval.common import Tools
|
11
|
+
|
12
|
+
|
13
|
+
mylogger = setup_logger(__name__,'WARNING')
|
14
|
+
expire_time = tools.to_int(os.getenv('DEFAULT_EXPIRE_TIME_H', 48)) * 3600
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class RedData:
|
19
|
+
"""
|
20
|
+
A data structure for financial data representation and calculations.
|
21
|
+
|
22
|
+
This class is designed to encapsulate financial details related to a company,
|
23
|
+
including calculations for business value, property value, debt evaluation, and
|
24
|
+
associated metrics. It validates specific attributes upon initialization and is
|
25
|
+
useful for financial data analysis.
|
26
|
+
|
27
|
+
Attributes:
|
28
|
+
code (str): A 6-digit numeric string representing the company or entity's code.
|
29
|
+
name (str): The name of the company or entity.
|
30
|
+
사업가치 (float): Business value calculated as net income attributable to controlling
|
31
|
+
shareholders divided by expected return rate.
|
32
|
+
지배주주당기순이익 (float): Net income attributable to controlling shareholders.
|
33
|
+
expect_earn (float): Expected return rate.
|
34
|
+
재산가치 (float): Property value calculated as current assets minus 1.2
|
35
|
+
times current liabilities, plus fixed assets under investment properties.
|
36
|
+
유동자산 (float): Current assets of the company.
|
37
|
+
유동부채 (float): Current liabilities of the company.
|
38
|
+
투자자산 (float): Investment assets within fixed assets.
|
39
|
+
투자부동산 (float): Investment real estate property.
|
40
|
+
부채평가 (float): Debt evaluation, specifically focusing on non-current liabilities.
|
41
|
+
발행주식수 (int): Number of issued shares by the company.
|
42
|
+
date (list): List of dates relevant to the financial data.
|
43
|
+
red_price (float): Red price associated with the company or entity.
|
44
|
+
score (int): Score or rating given to the company or entity.
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
ValueError: If the 'code' attribute is not a 6-digit numeric string.
|
48
|
+
"""
|
49
|
+
code: str
|
50
|
+
name: str
|
51
|
+
|
52
|
+
# 사업가치 계산 - 지배주주지분 당기순이익 / 기대수익률
|
53
|
+
사업가치: float
|
54
|
+
지배주주당기순이익: float
|
55
|
+
expect_earn: float
|
56
|
+
|
57
|
+
# 재산가치 계산 - 유동자산 - (유동부채*1.2) + 고정자산중 투자자산
|
58
|
+
재산가치: float
|
59
|
+
유동자산: float
|
60
|
+
유동부채: float
|
61
|
+
투자자산: float
|
62
|
+
투자부동산: float
|
63
|
+
|
64
|
+
# 부채평가 - 비유동부채
|
65
|
+
부채평가: float
|
66
|
+
|
67
|
+
# 발행주식수
|
68
|
+
발행주식수: int
|
69
|
+
|
70
|
+
date: list
|
71
|
+
|
72
|
+
red_price: float
|
73
|
+
score: int
|
74
|
+
|
75
|
+
def __post_init__(self):
|
76
|
+
if not tools.is_6digit(self.code):
|
77
|
+
raise ValueError(f"code는 6자리 숫자형 문자열이어야합니다. (입력값: {self.code})")
|
78
|
+
|
79
|
+
|
80
|
+
class Red:
|
81
|
+
"""
|
82
|
+
Represents a financial analysis object with methods to calculate metrics
|
83
|
+
and gather data related to a specific code.
|
84
|
+
|
85
|
+
The Red class is designed to interact with specific data sources and provide
|
86
|
+
tools for financial calculations and analysis. This includes fetching and
|
87
|
+
processing information related to liabilities, assets, stock prices, and
|
88
|
+
other financial indicators. The class facilitates both specific calculations
|
89
|
+
such as 비유동부채(Non-current Liability) and the generation of comprehensive
|
90
|
+
financial datasets.
|
91
|
+
"""
|
92
|
+
|
93
|
+
|
94
|
+
def __init__(self, code: str, expect_earn: float = 0.06):
|
95
|
+
assert tools.is_6digit(code), f'Invalid value : {code}'
|
96
|
+
mylogger.debug(f"Red : 초기화 ({code})")
|
97
|
+
self.c101 = myredis.C101(code)
|
98
|
+
self.c103 = myredis.C103(code, 'c103재무상태표q')
|
99
|
+
|
100
|
+
self.name = self.c101.get_name()
|
101
|
+
self._code = code
|
102
|
+
|
103
|
+
self.expect_earn = expect_earn
|
104
|
+
|
105
|
+
def __str__(self):
|
106
|
+
return f"Red({self.code}/{self.name})"
|
107
|
+
|
108
|
+
@property
|
109
|
+
def code(self) -> str:
|
110
|
+
return self._code
|
111
|
+
|
112
|
+
@code.setter
|
113
|
+
def code(self, code: str):
|
114
|
+
assert tools.is_6digit(code), f'Invalid value : {code}'
|
115
|
+
mylogger.debug(f"Red : 종목코드 변경({self.code} -> {code})")
|
116
|
+
self.c101.code = code
|
117
|
+
self.c103.code = code
|
118
|
+
|
119
|
+
self.name = self.c101.get_name()
|
120
|
+
self._code = code
|
121
|
+
|
122
|
+
def _calc비유동부채(self, refresh: bool) -> Tuple[str, float]:
|
123
|
+
"""유효한 비유동부채 계산
|
124
|
+
|
125
|
+
일반적인 경우로 비유동부채를 찾아서 반환한다.\n
|
126
|
+
금융기관의 경우는 간접적으로 계산한다.\n
|
127
|
+
"""
|
128
|
+
mylogger.info(f'In the calc비유동부채... refresh : {refresh}')
|
129
|
+
self.c103.page = 'c103재무상태표q'
|
130
|
+
|
131
|
+
d, 비유동부채 = self.c103.sum_recent_4q('비유동부채', refresh)
|
132
|
+
if math.isnan(비유동부채):
|
133
|
+
mylogger.warning(f"{self} - 비유동부채가 없는 종목. 수동으로 계산합니다.")
|
134
|
+
# 보험관련업종은 예수부채가 없는대신 보험계약부채가 있다...
|
135
|
+
d1, v1 = self.c103.latest_value_pop2('예수부채', refresh)
|
136
|
+
d2, v2 = self.c103.latest_value_pop2('보험계약부채(책임준비금)', refresh)
|
137
|
+
d3, v3 = self.c103.latest_value_pop2('차입부채', refresh)
|
138
|
+
d4, v4 = self.c103.latest_value_pop2('기타부채', refresh)
|
139
|
+
mylogger.debug(f'예수부채 : {d1}, {v1}')
|
140
|
+
mylogger.debug(f'보험계약부채(책임준비금) : {d2}, {v2}')
|
141
|
+
mylogger.debug(f'차입부채 : {d3}, {v3}')
|
142
|
+
mylogger.debug(f'기타부채 : {d4}, {v4}')
|
143
|
+
|
144
|
+
try:
|
145
|
+
date, *_ = Tools.date_set(d1, d2, d3, d4)
|
146
|
+
except ValueError:
|
147
|
+
# 날짜 데이터가 없는경우
|
148
|
+
date = ''
|
149
|
+
계산된비유동부채value = round(tools.nan_to_zero(v1) + tools.nan_to_zero(v2) + tools.nan_to_zero(v3) + tools.nan_to_zero(v4),1)
|
150
|
+
mylogger.info(f"{self} - 계산된 비유동부채 : {계산된비유동부채value}")
|
151
|
+
return date, 계산된비유동부채value
|
152
|
+
else:
|
153
|
+
return d, 비유동부채
|
154
|
+
|
155
|
+
def _score(self, red_price: int, refresh: bool) -> int:
|
156
|
+
"""red price와 최근 주가의 괴리율 파악
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
int : 주가와 red price 비교한 괴리율
|
160
|
+
"""
|
161
|
+
try:
|
162
|
+
recent_price = tools.to_float(self.c101.get_recent(refresh)['주가'])
|
163
|
+
except KeyError:
|
164
|
+
return 0
|
165
|
+
|
166
|
+
deviation = Tools.cal_deviation(recent_price, red_price)
|
167
|
+
if red_price < 0 or (recent_price >= red_price):
|
168
|
+
score = 0
|
169
|
+
else:
|
170
|
+
score = tools.to_int(math.log10(deviation + 1) * 33) # desmos그래프상 33이 제일 적당한듯(최대100점에 가깝게)
|
171
|
+
|
172
|
+
mylogger.debug(f"최근주가 : {recent_price} red가격 : {red_price} 괴리율 : {tools.to_int(deviation)} score : {score}")
|
173
|
+
|
174
|
+
return score
|
175
|
+
|
176
|
+
def _generate_data(self, refresh: bool) -> RedData:
|
177
|
+
d1, 지배주주당기순이익 = Tools.calc당기순이익(self.c103, refresh)
|
178
|
+
mylogger.debug(f"{self} 지배주주당기순이익: {지배주주당기순이익}")
|
179
|
+
d2, 유동자산 = Tools.calc유동자산(self.c103, refresh)
|
180
|
+
d3, 유동부채 = Tools.calc유동부채(self.c103, refresh)
|
181
|
+
d4, 부채평가 = self._calc비유동부채(refresh)
|
182
|
+
|
183
|
+
self.c103.page = 'c103재무상태표q'
|
184
|
+
d5, 투자자산 = self.c103.latest_value_pop2('투자자산', refresh)
|
185
|
+
d6, 투자부동산 = self.c103.latest_value_pop2('투자부동산', refresh)
|
186
|
+
|
187
|
+
# 사업가치 계산 - 지배주주지분 당기순이익 / 기대수익률
|
188
|
+
사업가치 = round(지배주주당기순이익 / self.expect_earn, 2)
|
189
|
+
|
190
|
+
# 재산가치 계산 - 유동자산 - (유동부채*1.2) + 고정자산중 투자자산
|
191
|
+
재산가치 = round(유동자산 - (유동부채 * 1.2) + tools.nan_to_zero(투자자산) + tools.nan_to_zero(투자부동산), 2)
|
192
|
+
|
193
|
+
_, 발행주식수 = self.c103.latest_value_pop2('발행주식수', refresh)
|
194
|
+
if math.isnan(발행주식수):
|
195
|
+
발행주식수 = tools.to_int(self.c101.get_recent(refresh).get('발행주식'))
|
196
|
+
else:
|
197
|
+
발행주식수 = 발행주식수 * 1000
|
198
|
+
|
199
|
+
try:
|
200
|
+
red_price = round(((사업가치 + 재산가치 - 부채평가) * 100000000) / 발행주식수)
|
201
|
+
except (ZeroDivisionError, ValueError):
|
202
|
+
red_price = math.nan
|
203
|
+
|
204
|
+
score = self._score(red_price, refresh)
|
205
|
+
|
206
|
+
try:
|
207
|
+
date_list = Tools.date_set(d1, d2, d3, d4)
|
208
|
+
except ValueError:
|
209
|
+
# 날짜 데이터가 없는경우
|
210
|
+
date_list = ['',]
|
211
|
+
|
212
|
+
return RedData(
|
213
|
+
code = self.code,
|
214
|
+
name = self.name,
|
215
|
+
사업가치 = 사업가치,
|
216
|
+
지배주주당기순이익 = 지배주주당기순이익,
|
217
|
+
expect_earn = self.expect_earn,
|
218
|
+
재산가치 = 재산가치,
|
219
|
+
유동자산 = 유동자산,
|
220
|
+
유동부채 = 유동부채,
|
221
|
+
투자자산 = 투자자산,
|
222
|
+
투자부동산 = 투자부동산,
|
223
|
+
부채평가 = 부채평가,
|
224
|
+
발행주식수 = 발행주식수,
|
225
|
+
date = date_list,
|
226
|
+
red_price = red_price,
|
227
|
+
score = score,
|
228
|
+
)
|
229
|
+
|
230
|
+
def get(self, refresh = False, verbose = True) -> RedData:
|
231
|
+
"""
|
232
|
+
RedData 형식의 데이터를 계산하여 리턴하고 레디스 캐시에 저장한다.
|
233
|
+
|
234
|
+
redis_name = f"{self.code}_red_data"
|
235
|
+
|
236
|
+
Fetch or create RedData from Redis cache.
|
237
|
+
|
238
|
+
This function attempts to retrieve the RedData from a Redis cache. If the data is
|
239
|
+
not available or if a refresh is requested, it generates new data and caches
|
240
|
+
them back in Redis. The function logs its operations and can provide
|
241
|
+
verbose output when specified.
|
242
|
+
|
243
|
+
Parameters:
|
244
|
+
refresh : bool, optional
|
245
|
+
Whether to refresh and generate new data instead of using the cached data.
|
246
|
+
verbose : bool, optional
|
247
|
+
Whether to enable verbose logging/display of additional runtime information.
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
RedData
|
251
|
+
The RedData object either retrieved from the cache or newly generated.
|
252
|
+
|
253
|
+
"""
|
254
|
+
redis_name = f"{self.code}_red_data"
|
255
|
+
mylogger.info(f"{self} RedData를 레디스캐시에서 가져오거나 새로 생성합니다.. refresh : {refresh}")
|
256
|
+
if verbose:
|
257
|
+
print(f"{self} redisname: '{redis_name}' / expect_earn: {self.expect_earn} / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
258
|
+
|
259
|
+
def fetch_generate_data(refresh_in: bool) -> dict:
|
260
|
+
return asdict(self._generate_data(refresh_in))
|
261
|
+
|
262
|
+
return RedData(**myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_generate_data, refresh, timer=expire_time))
|
263
|
+
|
264
|
+
@classmethod
|
265
|
+
def ranking(cls, expect_earn: float = 0.06, refresh = False) -> OrderedDict:
|
266
|
+
# 이전 expect earn 과 비교하여 다르거나 없으면 강제 refresh 설정
|
267
|
+
redis_name = 'red_ranking_prev_expect_earn'
|
268
|
+
pee = tools.to_float(myredis.Base.get_value(redis_name))
|
269
|
+
if pee != expect_earn:
|
270
|
+
# expect earn의 이전 계산값이 없거나 이전 값과 다르면 새로 계산
|
271
|
+
mylogger.warning(
|
272
|
+
f"expect earn : {expect_earn} / prev expect earn : {pee} 두 값이 달라 refresh = True"
|
273
|
+
)
|
274
|
+
myredis.Base.set_value(redis_name, str(expect_earn))
|
275
|
+
refresh = True
|
276
|
+
|
277
|
+
print("**** Start red_ranking... ****")
|
278
|
+
redis_name = 'red_ranking'
|
279
|
+
print(f"redisname: '{redis_name}' / expect_earn: {expect_earn} / refresh : {refresh} / expire_time : {expire_time/3600}h")
|
280
|
+
|
281
|
+
def fetch_ranking(refresh_in: bool) -> dict:
|
282
|
+
data = {}
|
283
|
+
red = Red(code='005930', expect_earn=expect_earn)
|
284
|
+
for i, code in enumerate(myredis.Corps.list_all_codes()):
|
285
|
+
red.code = code
|
286
|
+
red_score = red.get(refresh=refresh_in, verbose=False).score
|
287
|
+
if red_score > 0:
|
288
|
+
data[code] = red_score
|
289
|
+
print(f"{i}: {red} - {red_score}")
|
290
|
+
return data
|
291
|
+
|
292
|
+
data_dict = myredis.Base.fetch_and_cache_data(redis_name, refresh, fetch_ranking, refresh, timer=expire_time)
|
293
|
+
|
294
|
+
return OrderedDict(sorted(data_dict.items(), key=lambda item: item[1], reverse=True))
|
295
|
+
|