analyser_hj3415 2.0.0__py2.py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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