analyser_hj3415 2.10.6__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- analyser_hj3415/__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} +24 -23
- 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.6.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.6.dist-info/RECORD +0 -14
- {analyser_hj3415-2.10.6.dist-info → analyser_hj3415-3.0.0.dist-info}/WHEEL +0 -0
- {analyser_hj3415-2.10.6.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
|
+
|