analyser_hj3415 2.0.0__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.
- analyser_hj3415/.DS_Store +0 -0
- analyser_hj3415/__init__.py +0 -0
- analyser_hj3415/analyser/.DS_Store +0 -0
- analyser_hj3415/analyser/__init__.py +0 -0
- analyser_hj3415/analyser/cli.py +109 -0
- analyser_hj3415/analyser/db/.DS_Store +0 -0
- analyser_hj3415/analyser/db/__init__.py +0 -0
- analyser_hj3415/analyser/db/chk_db.py +240 -0
- analyser_hj3415/analyser/db/evaltools.py +257 -0
- analyser_hj3415/analyser/db/mongo.py +934 -0
- analyser_hj3415/analyser/eval.py +382 -0
- analyser_hj3415/analyser/report.py +218 -0
- analyser_hj3415/analyser/score.py +369 -0
- analyser_hj3415-2.0.0.dist-info/LICENSE +21 -0
- analyser_hj3415-2.0.0.dist-info/METADATA +232 -0
- analyser_hj3415-2.0.0.dist-info/RECORD +18 -0
- analyser_hj3415-2.0.0.dist-info/WHEEL +5 -0
- analyser_hj3415-2.0.0.dist-info/entry_points.txt +4 -0
Binary file
|
File without changes
|
Binary file
|
File without changes
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import argparse
|
2
|
+
from utils_hj3415 import utils
|
3
|
+
from .db import chk_db, mongo
|
4
|
+
from . import eval, report
|
5
|
+
from scraper2_hj3415.nfscrapy import run as nfsrun
|
6
|
+
|
7
|
+
|
8
|
+
def dbmanager():
|
9
|
+
cmd = ['repair', 'sync', 'eval', 'update']
|
10
|
+
parser = argparse.ArgumentParser()
|
11
|
+
parser.add_argument('cmd', help=f"Command - {cmd}")
|
12
|
+
parser.add_argument('target', help="Target for scraping (type 6digit code or 'all' or 'parts')")
|
13
|
+
parser.add_argument('-d', '--db_path', help="Set mongo database path")
|
14
|
+
|
15
|
+
args = parser.parse_args()
|
16
|
+
|
17
|
+
db_path = args.db_path if args.db_path else "mongodb://192.168.0.173:27017"
|
18
|
+
client = mongo.connect_mongo(db_path)
|
19
|
+
|
20
|
+
if args.cmd in cmd:
|
21
|
+
if args.cmd == 'repair':
|
22
|
+
if args.target == 'all' or utils.is_6digit(args.target):
|
23
|
+
need_for_repair_codes = chk_db.chk_integrity_corps(client, args.target)
|
24
|
+
# repair dict 예시 - {'343510': ['c106', 'c104', 'c103'], '298000': ['c104'], '091810': ['c104']}
|
25
|
+
print(f"Need for repairing codes :{need_for_repair_codes}")
|
26
|
+
if need_for_repair_codes:
|
27
|
+
# x = input("Do you want to try to repair db by scraping? (y/N)")
|
28
|
+
# if x == 'y' or x == 'Y':
|
29
|
+
for code, failed_page_list in need_for_repair_codes.items():
|
30
|
+
for page in failed_page_list:
|
31
|
+
if page == 'c101':
|
32
|
+
nfsrun.c101([code, ], db_path)
|
33
|
+
elif page == 'c103':
|
34
|
+
nfsrun.c103([code, ], db_path)
|
35
|
+
elif page == 'c104':
|
36
|
+
nfsrun.c104([code, ], db_path)
|
37
|
+
elif page == 'c106':
|
38
|
+
nfsrun.c106([code, ], db_path)
|
39
|
+
recheck_result = chk_db.chk_integrity_corps(client, code)
|
40
|
+
if recheck_result:
|
41
|
+
# 다시 스크랩해도 오류가 지속되는 경우
|
42
|
+
print(f"The db integrity failure persists..{recheck_result}")
|
43
|
+
# x = input(f"Do you want to delete {code} on DB? (y/N)")
|
44
|
+
# if x == 'y' or x == 'Y':
|
45
|
+
# mongo.Corps.del_db(client, code)
|
46
|
+
# else:
|
47
|
+
# print("Canceled.")
|
48
|
+
mongo.Corps.del_db(client, code)
|
49
|
+
# else:
|
50
|
+
# print("Done.")
|
51
|
+
else:
|
52
|
+
print("Done.")
|
53
|
+
else:
|
54
|
+
print(f"Invalid target option : {args.target}")
|
55
|
+
elif args.cmd == 'update':
|
56
|
+
if args.target == 'all' or utils.is_6digit(args.target):
|
57
|
+
need_for_update_codes = list(chk_db.chk_modifying_corps(client, args.target).keys())
|
58
|
+
# need_for_update_codes 예시 - [codes....]
|
59
|
+
print(f"Need for updating codes :{need_for_update_codes}")
|
60
|
+
if need_for_update_codes:
|
61
|
+
nfsrun.c103(need_for_update_codes, db_path)
|
62
|
+
nfsrun.c104(need_for_update_codes, db_path)
|
63
|
+
nfsrun.c106(need_for_update_codes, db_path)
|
64
|
+
elif args.target == 'parts':
|
65
|
+
pass
|
66
|
+
else:
|
67
|
+
print(f"Invalid target option : {args.target}")
|
68
|
+
elif args.cmd == 'sync':
|
69
|
+
if args.target == 'all':
|
70
|
+
chk_db.sync_mongo_with_krx(client)
|
71
|
+
else:
|
72
|
+
print(f"The target should be 'all' in sync command.")
|
73
|
+
elif args.cmd == 'eval':
|
74
|
+
if args.target == 'all':
|
75
|
+
# eval을 평가해서 데이터베이스에 저장한다.
|
76
|
+
eval.make_today_eval_df(client, refresh=True)
|
77
|
+
else:
|
78
|
+
print(f"The target should be 'all' in sync command.")
|
79
|
+
else:
|
80
|
+
print(f"The command should be in {cmd}")
|
81
|
+
|
82
|
+
client.close()
|
83
|
+
|
84
|
+
|
85
|
+
def evalmanager():
|
86
|
+
cmd = ['report', ]
|
87
|
+
parser = argparse.ArgumentParser()
|
88
|
+
parser.add_argument('cmd', help=f"Command - {cmd}")
|
89
|
+
parser.add_argument('target', help="Target for scraping (type 6digit code or 'all' or 'parts')")
|
90
|
+
parser.add_argument('-d', '--db_path', help="Set mongo database path")
|
91
|
+
|
92
|
+
args = parser.parse_args()
|
93
|
+
|
94
|
+
db_path = args.db_path if args.db_path else "mongodb://192.168.0.173:27017"
|
95
|
+
client = mongo.connect_mongo(db_path)
|
96
|
+
|
97
|
+
if args.cmd in cmd:
|
98
|
+
if args.cmd == 'report':
|
99
|
+
if utils.is_6digit(args.target):
|
100
|
+
print(report.Report(client, args.target))
|
101
|
+
else:
|
102
|
+
print(f"Invalid target option : {args.target}")
|
103
|
+
else:
|
104
|
+
print(f"The command should be in {cmd}")
|
105
|
+
|
106
|
+
|
107
|
+
if __name__ == "__main__":
|
108
|
+
# dbmanager()
|
109
|
+
evalmanager()
|
Binary file
|
File without changes
|
@@ -0,0 +1,240 @@
|
|
1
|
+
import time
|
2
|
+
import sys
|
3
|
+
from _datetime import datetime
|
4
|
+
from typing import Dict, List, Tuple, Callable
|
5
|
+
from multiprocessing import Process, Queue
|
6
|
+
from . import mongo
|
7
|
+
from utils_hj3415 import utils, noti
|
8
|
+
from scraper2_hj3415.krx import krx
|
9
|
+
from scraper2_hj3415.nfscrapy import run as nfsrun
|
10
|
+
from pymongo import MongoClient
|
11
|
+
from selenium.webdriver.chrome.webdriver import WebDriver
|
12
|
+
|
13
|
+
import logging
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
formatter = logging.Formatter('%(levelname)s: [%(name)s] %(message)s')
|
16
|
+
ch = logging.StreamHandler()
|
17
|
+
ch.setFormatter(formatter)
|
18
|
+
logger.addHandler(ch)
|
19
|
+
logger.setLevel(logging.INFO)
|
20
|
+
|
21
|
+
|
22
|
+
"""
|
23
|
+
chk_integrity_corps 함수로 종목코드를 데이터베이스명으로 가지는 DB의 유효성을 검사한다.
|
24
|
+
"""
|
25
|
+
|
26
|
+
|
27
|
+
def test_corp_one(client1: MongoClient, code: str, driver: WebDriver = None, waiting_time: int = 10) -> Dict[str, list]:
|
28
|
+
"""
|
29
|
+
종목 하나의 컬렉션의 유효성을 검사하여 부족한 컬렉션을 딕셔너리로 만들어서 반환한다.
|
30
|
+
driver와 waiting_time은 본 함수에서 사용하지는 않으나 다른 함수와 인자를 맞추기위해 인자로 받아준다.
|
31
|
+
리턴값 - {'005930': ['c104','c103'...]}
|
32
|
+
"""
|
33
|
+
|
34
|
+
def is_same_count_of_docs(col_name1: str, col_name2: str) -> bool:
|
35
|
+
logger.debug(f"In is_same_count_of_docs {code}/ {col_name1}, {col_name2}")
|
36
|
+
corp_one.page = col_name1
|
37
|
+
count_doc1 = corp_one.count_docs_in_col()
|
38
|
+
corp_one.page = col_name2
|
39
|
+
count_doc2 = corp_one.count_docs_in_col()
|
40
|
+
if count_doc1 == count_doc2:
|
41
|
+
return True
|
42
|
+
else:
|
43
|
+
return False
|
44
|
+
|
45
|
+
proper_collections = {'c101', 'c104y', 'c104q', 'c106y', 'c106q', 'c103손익계산서q', 'c103재무상태표q',
|
46
|
+
'c103현금흐름표q', 'c103손익계산서y', 'c103재무상태표y', 'c103현금흐름표y'}
|
47
|
+
|
48
|
+
logger.debug('In test_corp_one function...')
|
49
|
+
return_dict = {}
|
50
|
+
|
51
|
+
logger.debug(f'return_dict is ... {return_dict}')
|
52
|
+
# 한 종목의 유효성 검사코드
|
53
|
+
corp_one = mongo.Corps(client1, code, 'c101')
|
54
|
+
|
55
|
+
# 차집합을 사용해서 db내에 없는 컬렉션이 있는지 확인한다.
|
56
|
+
set_deficient_collentions = set.difference(proper_collections, set(corp_one.list_collection_names()))
|
57
|
+
|
58
|
+
logger.debug(f'After take a set of difference : {set_deficient_collentions}')
|
59
|
+
|
60
|
+
return_dict[code] = set()
|
61
|
+
# 컬렉션이 아예 없는 것이 있다면 falied_codes에 추가한다.
|
62
|
+
if set_deficient_collentions != set():
|
63
|
+
for item in set_deficient_collentions:
|
64
|
+
# 컬렉션 이름 중 앞의 네글자만 추려서 추가해준다.(ex - c103손익계산서q -> c103)
|
65
|
+
return_dict[code].add(item[:4])
|
66
|
+
|
67
|
+
# 각 컬렉션의 q와 y의 도큐먼트 갯수를 비교하여 차이가 있는지 확인한다.
|
68
|
+
if not is_same_count_of_docs('c104y', 'c104q'):
|
69
|
+
return_dict[code].add('c104')
|
70
|
+
if not is_same_count_of_docs('c106y', 'c106q'):
|
71
|
+
return_dict[code].add('c106')
|
72
|
+
if not is_same_count_of_docs('c103손익계산서q', 'c103손익계산서y') \
|
73
|
+
or not is_same_count_of_docs('c103재무상태표q', 'c103재무상태표y') \
|
74
|
+
or not is_same_count_of_docs('c103현금흐름표y', 'c103현금흐름표q'):
|
75
|
+
return_dict[code].add('c103')
|
76
|
+
|
77
|
+
# 집합을 리스트로 바꿔서 다시 저장한다.
|
78
|
+
return_dict[code] = list(return_dict[code])
|
79
|
+
logger.debug(f'Going out test_corp_one : {return_dict}')
|
80
|
+
return return_dict
|
81
|
+
|
82
|
+
|
83
|
+
def test_corp_one_is_modified(client: MongoClient, code: str, driver: WebDriver, waiting_time: int) -> Dict[str, bool]:
|
84
|
+
"""
|
85
|
+
웹에서 스크랩한 c103손익계산서y와 데이터베이스에 있는 c103손익계산서y를 비교하여 다른지 확인하여 업데이트 유무를 반환한다.
|
86
|
+
리턴값 - (코드, bool-업데이트가필요한지)
|
87
|
+
"""
|
88
|
+
df_online = nfsrun.scrape_c103_first_page(driver, code, waiting_time=waiting_time)
|
89
|
+
df_mongo = mongo.C103(client, code=code, page='c103손익계산서y').load_df()
|
90
|
+
|
91
|
+
logger.debug(df_online)
|
92
|
+
logger.debug(df_mongo)
|
93
|
+
|
94
|
+
return_dict = {code: not df_online.equals(df_mongo)}
|
95
|
+
return return_dict
|
96
|
+
|
97
|
+
|
98
|
+
def working_with_parts(test_func: Callable[[MongoClient, str, WebDriver, int], dict], db_addr: str, divided_code_list: list, my_q: Queue, waiting_time: int):
|
99
|
+
# 각 코어별로 디비 클라이언트를 만들어야만 한다. 안그러면 에러발생
|
100
|
+
client = mongo.connect_mongo(db_addr)
|
101
|
+
driver = utils.get_driver()
|
102
|
+
t = len(divided_code_list)
|
103
|
+
|
104
|
+
failed_dict_part = {}
|
105
|
+
|
106
|
+
for i, code in enumerate(divided_code_list):
|
107
|
+
try:
|
108
|
+
failed_one_dict = test_func(client, code, driver, waiting_time)
|
109
|
+
except Exception as e:
|
110
|
+
print(f"{code} has a error : {e}", file=sys.stderr)
|
111
|
+
continue
|
112
|
+
print(f'{i + 1}/{t} {failed_one_dict}')
|
113
|
+
if failed_one_dict[code]:
|
114
|
+
# 빈리스트가 아니라면...또는 C103이 변화되었다면.. 큐에 추가한다.
|
115
|
+
failed_dict_part.update(failed_one_dict)
|
116
|
+
else:
|
117
|
+
# 큐에서 put은 함수 리턴처럼 함수에서 한번만 한다.
|
118
|
+
my_q.put(failed_dict_part)
|
119
|
+
driver.close()
|
120
|
+
|
121
|
+
|
122
|
+
# 멀티프로세싱을 사용하기 위해서 독립된 함수로 제작하였음(피클링이 가능해야함)
|
123
|
+
def chk_integrity_corps(client: MongoClient, code: str = 'all') -> Dict[str, list]:
|
124
|
+
"""
|
125
|
+
몽고 디비의 corps들의 integrity 검사후 이상이 있는 코드 리스트 반환
|
126
|
+
이상을 찾는 방법 - 각 컬렉션이 다 있는가. 각 컬렉션에서 연도와 분기의 도큐먼트 갯수가 같은가
|
127
|
+
return - {'코드': ['cxxx',...], '코드': ['cxxx',...]...}
|
128
|
+
"""
|
129
|
+
failed_codes = {}
|
130
|
+
codes_in_db = mongo.Corps.get_all_codes(client)
|
131
|
+
if code == 'all':
|
132
|
+
print('*' * 25, f"Check all Corp db integrity using multiprocess", '*' * 25)
|
133
|
+
print(f'Total {len(codes_in_db)} items..')
|
134
|
+
n, divided_list = utils.code_divider_by_cpu_core(codes_in_db)
|
135
|
+
|
136
|
+
addr = mongo.extract_addr_from_client(client)
|
137
|
+
|
138
|
+
start_time = time.time()
|
139
|
+
q = Queue()
|
140
|
+
ths = []
|
141
|
+
for i in range(n):
|
142
|
+
ths.append(Process(target=working_with_parts, args=(test_corp_one, addr, divided_list[i], q, 0)))
|
143
|
+
for i in range(n):
|
144
|
+
ths[i].start()
|
145
|
+
|
146
|
+
for i in range(n):
|
147
|
+
failed_codes.update(q.get())
|
148
|
+
|
149
|
+
for i in range(n):
|
150
|
+
ths[i].join()
|
151
|
+
|
152
|
+
logger.debug(f"failed_codes : {failed_codes}")
|
153
|
+
print(f'Total spent time : {round(time.time() - start_time, 2)} sec.')
|
154
|
+
else:
|
155
|
+
print('*' * 25, f"Check {code} db integrity", '*' * 25)
|
156
|
+
if code in codes_in_db:
|
157
|
+
result_dict = test_corp_one(client, code)
|
158
|
+
print(f'{code} : {result_dict[code]}')
|
159
|
+
if result_dict[code]: # 빈리스트가 아니라면...
|
160
|
+
failed_codes.update(result_dict)
|
161
|
+
|
162
|
+
else:
|
163
|
+
Exception(f'{code} is not in db..')
|
164
|
+
return failed_codes
|
165
|
+
|
166
|
+
|
167
|
+
def chk_modifying_corps(client, code: str = 'all', waiting_time: int = 60) -> Dict[str, bool]:
|
168
|
+
"""
|
169
|
+
각 종목의 웹과 DB의 C103손익계산서y를 비교하여 변화가 있어 refresh가 필요한지를 반환한다.
|
170
|
+
"""
|
171
|
+
failed_codes = {}
|
172
|
+
codes_in_db = mongo.Corps.get_all_codes(client)
|
173
|
+
if code == 'all':
|
174
|
+
print('*' * 25, f"Check all Corp db need for updating using multiprocess", '*' * 25)
|
175
|
+
print(f'Total {len(codes_in_db)} items..')
|
176
|
+
n, divided_list = utils.code_divider_by_cpu_core(codes_in_db)
|
177
|
+
|
178
|
+
addr = mongo.extract_addr_from_client(client)
|
179
|
+
|
180
|
+
start_time = time.time()
|
181
|
+
q = Queue()
|
182
|
+
ths = []
|
183
|
+
for i in range(n):
|
184
|
+
ths.append(Process(target=working_with_parts, args=(test_corp_one_is_modified, addr, divided_list[i], q, waiting_time)))
|
185
|
+
for i in range(n):
|
186
|
+
ths[i].start()
|
187
|
+
|
188
|
+
for i in range(n):
|
189
|
+
failed_codes.update(q.get())
|
190
|
+
|
191
|
+
for i in range(n):
|
192
|
+
ths[i].join()
|
193
|
+
|
194
|
+
logger.debug(f"failed_codes : {failed_codes}")
|
195
|
+
print(f'Total spent time : {round(time.time() - start_time, 2)} sec.')
|
196
|
+
else:
|
197
|
+
print('*' * 25, f"Check {code} db need for updating ", '*' * 25)
|
198
|
+
driver = utils.get_driver()
|
199
|
+
if code in codes_in_db:
|
200
|
+
result_dict = test_corp_one_is_modified(client, code, driver)
|
201
|
+
print(f'{code} : {result_dict[code]}')
|
202
|
+
if result_dict[code]:
|
203
|
+
failed_codes.update(result_dict)
|
204
|
+
|
205
|
+
else:
|
206
|
+
Exception(f'{code} is not in db..')
|
207
|
+
return failed_codes
|
208
|
+
|
209
|
+
|
210
|
+
def sync_mongo_with_krx(client):
|
211
|
+
print('*' * 20, 'Sync with krx and mongodb', '*' * 20)
|
212
|
+
all_codes_in_db = mongo.Corps.get_all_codes(client)
|
213
|
+
print('*' * 20, 'Refreshing krx.db...', '*' * 20)
|
214
|
+
krx.make_db()
|
215
|
+
print('*' * 80)
|
216
|
+
all_codes_in_krx = krx.get_codes()
|
217
|
+
print('\tThe number of codes in krx: ', len(all_codes_in_krx))
|
218
|
+
logger.debug(all_codes_in_krx)
|
219
|
+
try:
|
220
|
+
print('\tThe number of dbs in mongo: ', len(all_codes_in_db))
|
221
|
+
logger.debug(all_codes_in_db)
|
222
|
+
except TypeError:
|
223
|
+
err_msg = "Error while sync mongo data...it's possible mongo db doesn't set yet.."
|
224
|
+
logger.error(err_msg)
|
225
|
+
noti.telegram_to(botname='manager', text=err_msg)
|
226
|
+
return
|
227
|
+
del_targets = list(set(all_codes_in_db) - set(all_codes_in_krx))
|
228
|
+
add_targets = list(set(all_codes_in_krx) - set(all_codes_in_db))
|
229
|
+
print('\tDelete target: ', del_targets)
|
230
|
+
print('\tAdd target: ', add_targets)
|
231
|
+
|
232
|
+
for target in del_targets:
|
233
|
+
mongo.Corps.del_db(client, target)
|
234
|
+
|
235
|
+
if add_targets:
|
236
|
+
print(f'Starting.. c10346 scraper.. items : {len(add_targets)}')
|
237
|
+
addr = mongo.extract_addr_from_client(client)
|
238
|
+
nfsrun.c103(add_targets, addr)
|
239
|
+
nfsrun.c104(add_targets, addr)
|
240
|
+
nfsrun.c106(add_targets, addr)
|
@@ -0,0 +1,257 @@
|
|
1
|
+
import math
|
2
|
+
from typing import Tuple
|
3
|
+
from collections import OrderedDict
|
4
|
+
|
5
|
+
from .mongo import C101, C103, C104, Corps
|
6
|
+
|
7
|
+
import logging
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
formatter = logging.Formatter('%(levelname)s: [%(name)s] %(message)s')
|
10
|
+
ch = logging.StreamHandler()
|
11
|
+
ch.setFormatter(formatter)
|
12
|
+
logger.addHandler(ch)
|
13
|
+
logger.setLevel(logging.WARNING)
|
14
|
+
|
15
|
+
|
16
|
+
def extract_valid_one(*args):
|
17
|
+
"""
|
18
|
+
유틸함수
|
19
|
+
딕셔너리 데이터를 입력받아 하나씩 pop 하여 빈데이터가 아닌 첫번째것을 반환한다.
|
20
|
+
"""
|
21
|
+
logger.debug("In extract_valid_one func...")
|
22
|
+
# 입력받은 데이터를 중복되는 것을 제하기 위해 집합으로 변환한다.
|
23
|
+
d_set = {i for i in args}
|
24
|
+
for i in d_set:
|
25
|
+
logger.debug(i)
|
26
|
+
# 하나씩 꺼내서 빈문자가 아니면 반환한다.
|
27
|
+
if i != "" and i is not math.nan and i is not None:
|
28
|
+
return i
|
29
|
+
else:
|
30
|
+
return None
|
31
|
+
|
32
|
+
|
33
|
+
def calc당기순이익(client, code: str) -> Tuple[str, float]:
|
34
|
+
"""지배지분 당기순이익 계산
|
35
|
+
|
36
|
+
일반적인 경우로는 직전 지배주주지분 당기순이익을 찾아서 반환한다.\n
|
37
|
+
금융기관의 경우는 지배당기순이익이 없기 때문에\n
|
38
|
+
계산을 통해서 간접적으로 구한다.\n
|
39
|
+
"""
|
40
|
+
logger.debug(f'In the calc당기순이익... code:{code}')
|
41
|
+
c103q = C103(client, code, 'c103재무상태표q')
|
42
|
+
try:
|
43
|
+
profit_dict = c103q.find(title='*(지배)당기순이익')
|
44
|
+
logger.info(f'*(지배)당기순이익 : {profit_dict}')
|
45
|
+
return c103q.latest_value('*(지배)당기순이익', nan_to_zero=True)
|
46
|
+
except:
|
47
|
+
# 금융관련은 재무상태표에 지배당기순이익이 없어서 손익계산서의 당기순이익에서 비지배당기순이익을 빼서 간접적으로 구한다.
|
48
|
+
c103q.page = 'c103손익계산서q'
|
49
|
+
최근당기순이익date, 최근당기순이익value = c103q.sum_recent_4q('당기순이익', nan_to_zero=True)
|
50
|
+
c103q.page = 'c103재무상태표q'
|
51
|
+
비지배당기순이익date, 비지배당기순이익value= c103q.latest_value('*(비지배)당기순이익', nan_to_zero=True, allow_empty=True)
|
52
|
+
|
53
|
+
date = extract_valid_one(최근당기순이익date, 비지배당기순이익date)
|
54
|
+
계산된지배당기순이익value = 최근당기순이익value - 비지배당기순이익value
|
55
|
+
|
56
|
+
return date, 계산된지배당기순이익value
|
57
|
+
|
58
|
+
|
59
|
+
def calc유동자산(client, code: str) -> Tuple[str, float]:
|
60
|
+
"""유효한 유동자산 계산
|
61
|
+
|
62
|
+
일반적인 경우로 유동자산을 찾아서 반환한다.\n
|
63
|
+
금융기관의 경우는 간접적으로 계산한다.\n
|
64
|
+
Red와 Blue에서 사용한다.\n
|
65
|
+
"""
|
66
|
+
logger.debug(f'In the calc유동자산... code:{code}')
|
67
|
+
c103q = C103(client, code, 'c103재무상태표q')
|
68
|
+
try:
|
69
|
+
asset_dict = c103q.find(title='유동자산')
|
70
|
+
logger.info(f'유동자산 : {asset_dict}')
|
71
|
+
return c103q.sum_recent_4q('유동자산', nan_to_zero=True)
|
72
|
+
except:
|
73
|
+
# 금융관련업종...
|
74
|
+
d1, v1 = c103q.latest_value('현금및예치금', nan_to_zero=True, allow_empty=True)
|
75
|
+
d2, v2 = c103q.latest_value('단기매매금융자산', nan_to_zero=True, allow_empty=True)
|
76
|
+
d3, v3 = c103q.latest_value('매도가능금융자산', nan_to_zero=True, allow_empty=True)
|
77
|
+
d4, v4 = c103q.latest_value('만기보유금융자산', nan_to_zero=True, allow_empty=True)
|
78
|
+
logger.debug(f'현금및예치금 : {d1}, {v1}')
|
79
|
+
logger.debug(f'단기매매금융자산 : {d2}, {v2}')
|
80
|
+
logger.debug(f'매도가능금융자산 : {d3}, {v3}')
|
81
|
+
logger.debug(f'만기보유금융자산 : {d4}, {v4}')
|
82
|
+
|
83
|
+
date = extract_valid_one(d1, d2, d3, d4)
|
84
|
+
계산된유동자산value = v1 + v2 + v3 + v4
|
85
|
+
|
86
|
+
return date, 계산된유동자산value
|
87
|
+
|
88
|
+
|
89
|
+
def calc유동부채(client, code: str) -> Tuple[str, float]:
|
90
|
+
"""유효한 유동부채 계산
|
91
|
+
|
92
|
+
일반적인 경우로 유동부채를 찾아서 반환한다.\n
|
93
|
+
금융기관의 경우는 간접적으로 계산한다.\n
|
94
|
+
Red와 Blue에서 사용한다.\n
|
95
|
+
"""
|
96
|
+
logger.debug(f'In the calc유동부채... code:{code}')
|
97
|
+
c103q = C103(client, code, 'c103재무상태표q')
|
98
|
+
try:
|
99
|
+
debt_dict = c103q.find(title='유동부채')
|
100
|
+
logger.debug(f'유동부채 : {debt_dict}')
|
101
|
+
return c103q.sum_recent_4q('유동부채', nan_to_zero=True)
|
102
|
+
except:
|
103
|
+
# 금융관련업종...
|
104
|
+
d1, v1 = c103q.latest_value('당기손익인식(지정)금융부채', nan_to_zero=True, allow_empty=True)
|
105
|
+
d2, v2 = c103q.latest_value('당기손익-공정가치측정금융부채', nan_to_zero=True, allow_empty=True)
|
106
|
+
d3, v3 = c103q.latest_value('매도파생결합증권', nan_to_zero=True, allow_empty=True)
|
107
|
+
d4, v4 = c103q.latest_value('단기매매금융부채', nan_to_zero=True, allow_empty=True)
|
108
|
+
logger.debug(f'당기손익인식(지정)금융부채 : {d1}, {v1}')
|
109
|
+
logger.debug(f'당기손익-공정가치측정금융부채 : {d2}, {v2}')
|
110
|
+
logger.debug(f'매도파생결합증권 : {d3}, {v3}')
|
111
|
+
logger.debug(f'단기매매금융부채 : {d4}, {v4}')
|
112
|
+
|
113
|
+
date = extract_valid_one(d1, d2, d3, d4)
|
114
|
+
계산된유동부채value = v1 + v2 + v3 + v4
|
115
|
+
|
116
|
+
return date, 계산된유동부채value
|
117
|
+
|
118
|
+
|
119
|
+
def calc비유동부채(client, code: str) -> Tuple[str, float]:
|
120
|
+
"""유효한 비유동부채 계산
|
121
|
+
|
122
|
+
일반적인 경우로 비유동부채를 찾아서 반환한다.\n
|
123
|
+
금융기관의 경우는 간접적으로 계산한다.\n
|
124
|
+
Red와 Blue에서 사용한다.\n
|
125
|
+
"""
|
126
|
+
logger.debug(f'In the calc비유동부채... code:{code}')
|
127
|
+
c103q = C103(client, code, 'c103재무상태표q')
|
128
|
+
try:
|
129
|
+
debt_dict = c103q.find(title='비유동부채')
|
130
|
+
logger.debug(f'비유동부채 : {debt_dict}')
|
131
|
+
return c103q.sum_recent_4q('비유동부채', nan_to_zero=True)
|
132
|
+
except:
|
133
|
+
# 금융관련업종...
|
134
|
+
# 보험관련업종은 예수부채가 없는대신 보험계약부채가 있다...
|
135
|
+
d1, v1 = c103q.latest_value('예수부채', nan_to_zero=True, allow_empty=True)
|
136
|
+
d2, v2 = c103q.latest_value('보험계약부채(책임준비금)', nan_to_zero=True, allow_empty=True)
|
137
|
+
d3, v3 = c103q.latest_value('차입부채', nan_to_zero=True, allow_empty=True)
|
138
|
+
d4, v4 = c103q.latest_value('기타부채', nan_to_zero=True, allow_empty=True)
|
139
|
+
logger.debug(f'예수부채 : {d1}, {v1}')
|
140
|
+
logger.debug(f'보험계약부채(책임준비금) : {d2}, {v2}')
|
141
|
+
logger.debug(f'차입부채 : {d3}, {v3}')
|
142
|
+
logger.debug(f'기타부채 : {d4}, {v4}')
|
143
|
+
|
144
|
+
date = extract_valid_one(d1, d2, d3, d4)
|
145
|
+
계산된비유동부채value = v1 + v2 + v3 + v4
|
146
|
+
|
147
|
+
return date, 계산된비유동부채value
|
148
|
+
|
149
|
+
|
150
|
+
def calc유동비율(client, code: str, pop_count: int) -> Tuple[str, float]:
|
151
|
+
"""유동비율계산 - Blue에서 사용
|
152
|
+
|
153
|
+
c104q에서 최근유동비율 찾아보고 유효하지 않거나 \n
|
154
|
+
100이하인 경우에는수동으로 계산해서 다시 한번 평가해 본다.\n
|
155
|
+
"""
|
156
|
+
logger.debug(f'In the calc유동비율... code:{code}')
|
157
|
+
c104q = C104(client, code, 'c104q')
|
158
|
+
유동비율date, 유동비율value = c104q.latest_value('유동비율', pop_count=pop_count, allow_empty=True)
|
159
|
+
logger.debug(f'{code} 유동비율 : {유동비율value}({유동비율date})')
|
160
|
+
|
161
|
+
if math.isnan(유동비율value) or 유동비율value < 100:
|
162
|
+
logger.warning('유동비율 is under 100 or nan..so we will recalculate..')
|
163
|
+
유동자산date, 유동자산value = calc유동자산(client, code)
|
164
|
+
유동부채date, 유동부채value = calc유동부채(client, code)
|
165
|
+
|
166
|
+
c103q = C103(client, code, 'c103현금흐름표q')
|
167
|
+
추정영업현금흐름date, 추정영업현금흐름value = c103q.sum_recent_4q('영업활동으로인한현금흐름')
|
168
|
+
logger.debug(f'{code} 계산전 유동비율 : {유동비율value}({유동비율date})')
|
169
|
+
|
170
|
+
계산된유동비율 = 0
|
171
|
+
try:
|
172
|
+
계산된유동비율 = round(((유동자산value + 추정영업현금흐름value) / 유동부채value) * 100, 2)
|
173
|
+
except ZeroDivisionError:
|
174
|
+
logger.debug(f'유동자산: {유동자산value} + 추정영업현금흐름: {추정영업현금흐름value} / 유동부채: {유동부채value}')
|
175
|
+
계산된유동비율 = float('inf')
|
176
|
+
finally:
|
177
|
+
logger.debug(f'{code} 계산된 유동비율 : {계산된유동비율}')
|
178
|
+
return extract_valid_one(유동자산date, 유동부채date, 추정영업현금흐름date), 계산된유동비율
|
179
|
+
else:
|
180
|
+
return 유동비율date, 유동비율value
|
181
|
+
|
182
|
+
|
183
|
+
def findFCF(client, code: str) -> dict:
|
184
|
+
"""FCF 계산
|
185
|
+
|
186
|
+
FCF = 영업활동현금흐름 - CAPEX\n
|
187
|
+
영업활동현금흐름에서 CAPEX 를 각 연도별로 빼주어 fcf 를 구하고 딕셔너리로 반환한다.\n
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
dict: 계산된 fcf 딕셔너리 또는 영업현금흐름 없는 경우 - {}
|
191
|
+
|
192
|
+
Note:
|
193
|
+
CAPEX 가 없는 업종은 영업활동현금흐름을 그대로 사용한다.\n
|
194
|
+
|
195
|
+
"""
|
196
|
+
c103y = C103(client, code, 'c103현금흐름표y')
|
197
|
+
영업활동현금흐름_dict = c103y.find(title='영업활동으로인한현금흐름', allow_empty=True)
|
198
|
+
c103y.page = 'c103재무상태표y'
|
199
|
+
capex = c103y.find(title='*CAPEX', allow_empty=True)
|
200
|
+
|
201
|
+
logger.debug(f'영업활동현금흐름 {영업활동현금흐름_dict}')
|
202
|
+
logger.debug(f'CAPEX {capex}')
|
203
|
+
|
204
|
+
if len(영업활동현금흐름_dict) == 0:
|
205
|
+
return {}
|
206
|
+
|
207
|
+
if len(capex) == 0:
|
208
|
+
# CAPEX 가 없는 업종은 영업활동현금흐름을 그대로 사용한다.
|
209
|
+
return 영업활동현금흐름_dict
|
210
|
+
|
211
|
+
# 영업 활동으로 인한 현금 흐름에서 CAPEX 를 각 연도별로 빼주어 fcf 를 구하고 리턴값으로 fcf 딕셔너리를 반환한다.
|
212
|
+
r_dict = {}
|
213
|
+
for i in range(len(영업활동현금흐름_dict)):
|
214
|
+
# 영업활동현금흐름에서 아이템을 하나씩 꺼내서 CAPEX 전체와 비교하여 같으면 차를 구해서 r_dict 에 추가한다.
|
215
|
+
영업활동현금흐름date, 영업활동현금흐름value = 영업활동현금흐름_dict.popitem()
|
216
|
+
# 해당 연도의 capex 가 없는 경우도 있어 일단 capex를 0으로 치고 먼저 추가한다.
|
217
|
+
r_dict[영업활동현금흐름date] = 영업활동현금흐름value
|
218
|
+
for CAPEXdate, CAPEXvalue in capex.items():
|
219
|
+
if 영업활동현금흐름date == CAPEXdate:
|
220
|
+
r_dict[영업활동현금흐름date] = round(영업활동현금흐름value - CAPEXvalue, 2)
|
221
|
+
logger.debug(f'r_dict {r_dict}')
|
222
|
+
# 연도순으로 정렬해서 딕셔너리로 반환한다.
|
223
|
+
return dict(sorted(r_dict.items(), reverse=False))
|
224
|
+
|
225
|
+
|
226
|
+
def findPFCF(client, code: str) -> dict:
|
227
|
+
"""Price to Free Cash Flow Ratio 계산
|
228
|
+
|
229
|
+
PFCF = 시가총액 / FCF
|
230
|
+
|
231
|
+
Note:
|
232
|
+
https://www.investopedia.com/terms/p/pricetofreecashflow.asp
|
233
|
+
"""
|
234
|
+
# marketcap 계산 (fcf가 억 단위라 시가총액을 억으로 나눠서 단위를 맞춰 준다)
|
235
|
+
marketcap억 = get_marketcap(client, code) / 100000000
|
236
|
+
if math.isnan(marketcap억):
|
237
|
+
return {}
|
238
|
+
|
239
|
+
# pfcf 계산
|
240
|
+
fcf_dict = findFCF(client, code)
|
241
|
+
logger.debug(f'fcf_dict : {fcf_dict}')
|
242
|
+
pfcf_dict = {}
|
243
|
+
for FCFdate, FCFvalue in fcf_dict.items():
|
244
|
+
if FCFvalue == 0:
|
245
|
+
pfcf_dict[FCFdate] = math.nan
|
246
|
+
else:
|
247
|
+
pfcf_dict[FCFdate] = round(marketcap억 / FCFvalue, 2)
|
248
|
+
logger.debug(f'pfcf_dict : {pfcf_dict}')
|
249
|
+
return pfcf_dict
|
250
|
+
|
251
|
+
|
252
|
+
def get_marketcap(client, code: str, nan_to_zero: bool = False) -> int:
|
253
|
+
c101 = C101(client, code)
|
254
|
+
try:
|
255
|
+
return int(c101.get_recent()['시가총액'])
|
256
|
+
except KeyError:
|
257
|
+
return 0 if nan_to_zero else math.nan
|