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.
- 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
|