analyser_hj3415 2.0.2__py2.py3-none-any.whl → 2.2.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.
@@ -1,934 +0,0 @@
1
- import pymongo
2
- import sys
3
-
4
- import copy
5
- from pymongo import errors, MongoClient
6
- import math
7
- import random
8
- import datetime
9
- from typing import List, Tuple
10
- from collections import OrderedDict
11
- from abc import *
12
- from utils_hj3415 import utils
13
- import pandas as pd
14
-
15
-
16
- import logging
17
- logger = logging.getLogger(__name__)
18
- formatter = logging.Formatter('%(levelname)s: [%(name)s] %(message)s')
19
- ch = logging.StreamHandler()
20
- ch.setFormatter(formatter)
21
- logger.addHandler(ch)
22
- logger.setLevel(logging.WARNING)
23
-
24
- """
25
- 몽고db구조
26
- RDBMS : database / tables / rows / columns
27
- MongoDB : database / collections / documents / fields
28
- """
29
-
30
-
31
- class UnableConnectServerException(Exception):
32
- """
33
- 몽고 서버 연결 에러를 처리하기 위한 커스텀 익셉션
34
- """
35
- pass
36
-
37
-
38
- def connect_mongo(addr: str, timeout=5) -> MongoClient:
39
- """
40
- 몽고 클라이언트를 만들어주는 함수.
41
- 필요할 때마다 클라이언트를 생성하는 것보다 클라이언트 한개로 데이터베이스를 다루는게 효율적이라 함수를 따로 뺐음.
42
- resolve conn error - https://stackoverflow.com/questions/54484890/ssl-handshake-issue-with-pymongo-on-python3
43
- :param addr:
44
- :param timeout:
45
- :return:
46
- """
47
- import certifi
48
- ca = certifi.where()
49
- if addr.startswith('mongodb://'):
50
- # set a some-second connection timeout
51
- client = pymongo.MongoClient(addr, serverSelectionTimeoutMS=timeout * 1000)
52
- elif addr.startswith('mongodb+srv://'):
53
- client = pymongo.MongoClient(addr, serverSelectionTimeoutMS=timeout * 1000, tlsCAFile=ca)
54
- else:
55
- raise Exception(f"Invalid address: {addr}")
56
- try:
57
- srv_info = client.server_info()
58
- conn_str = f"Connect to Mongo Atlas v{srv_info['version']}..."
59
- print(conn_str, f"Server Addr : {addr}")
60
- return client
61
- except:
62
- raise UnableConnectServerException()
63
-
64
-
65
- def extract_addr_from_client(client: MongoClient) -> str:
66
- """
67
- scaraper는 클라이언트를 인자로 받지않고 주소를 받아서 사용하기 때문에 사용하는 주소 추출함수
68
- """
69
- # client에서 mongodb주소 추출
70
- ip = str(list(client.nodes)[0][0])
71
- port = str(list(client.nodes)[0][1])
72
- return 'mongodb://' + ip + ':' + port
73
-
74
-
75
- class MongoBase:
76
- def __init__(self, client: MongoClient, db_name: str, col_name: str):
77
- self._db_name = "test"
78
- self._col_name = "test"
79
- self.client = client
80
- self.db_name = db_name
81
- self.col_name = col_name
82
-
83
- @property
84
- def db_name(self):
85
- return self._db_name
86
-
87
- @db_name.setter
88
- def db_name(self, db_name):
89
- if self.client is None:
90
- raise Exception("You should set server connection first")
91
- else:
92
- self._db_name = db_name
93
- self.my_db = self.client[self.db_name]
94
- self.my_col = self.my_db[self.col_name]
95
-
96
- @property
97
- def col_name(self):
98
- return self._col_name
99
-
100
- @col_name.setter
101
- def col_name(self, col_name):
102
- if self.db_name is None:
103
- raise Exception("You should set database first.")
104
- else:
105
- self._col_name = col_name
106
- self.my_col = self.my_db[self.col_name]
107
-
108
- # ========================End Properties=======================
109
-
110
- @staticmethod
111
- def get_all_db_name(client: MongoClient) -> list:
112
- return sorted(client.list_database_names())
113
-
114
- @staticmethod
115
- def validate_db(client: MongoClient, db_name: str) -> bool:
116
- """
117
- db_name 이 실제로 몽고db 안에 있는지 확인한다.
118
- """
119
- if db_name in client.list_database_names():
120
- return True
121
- else:
122
- return False
123
-
124
- @staticmethod
125
- def del_db(client: MongoClient, db_name: str):
126
- if MongoBase.validate_db(client, db_name):
127
- client.drop_database(db_name)
128
- print(f"Drop '{db_name}' database..")
129
- else:
130
- print(f"Invalid db name : {db_name}", file=sys.stderr)
131
-
132
- def validate_col(self) -> bool:
133
- """
134
- col_name 이 실제로 db 안에 있는지 확인한다.
135
- """
136
- if self.validate_db(self.client, self.db_name):
137
- if self.col_name in self.my_db.list_collection_names():
138
- return True
139
- else:
140
- return False
141
-
142
- def get_all_docs(self, remove_id=True) -> list:
143
- """
144
- 현재 설정된 컬렉션 안의 도큐먼트 전부를 리스트로 반환한다.
145
- """
146
- items = []
147
- if remove_id:
148
- for doc in self.my_col.find({}):
149
- del doc['_id']
150
- items.append(doc)
151
- else:
152
- items = list(self.my_col.find({}))
153
- return items
154
-
155
- def count_docs_in_col(self) -> int:
156
- """
157
- 현재 설정된 컬렉션 안의 도큐먼트의 갯수를 반환한다.
158
- """
159
- return self.my_col.count_documents({})
160
-
161
- def clear_docs_in_col(self):
162
- """
163
- 현재 설정된 컬렉션 안의 도큐먼트를 전부 삭제한다.
164
- (컬렉션 자체를 삭제하지는 않는다.)
165
- """
166
- self.my_col.delete_many({})
167
- print(f"Delete all doccument in {self.col_name} collection..")
168
-
169
- def del_col(self):
170
- """
171
- 현재 설정된 컬렉션을 삭제한다.
172
- """
173
- self.my_db.drop_collection(self.col_name)
174
- print(f"Drop {self.col_name} collection..")
175
-
176
- def list_collection_names(self) -> list:
177
- return self.my_db.list_collection_names()
178
-
179
- def del_doc(self, del_query: dict):
180
- """
181
- del_query에 해당하는 도큐먼트를 삭제한다.
182
- """
183
- self.my_col.delete_one(del_query)
184
-
185
-
186
- class Corps(MongoBase):
187
- """
188
- mongodb의 데이터 중 기업 코드로 된 데이터 베이스를 다루는 클래스
189
- """
190
- COLLECTIONS = ('c101', 'c104y', 'c104q', 'c106y', 'c106q', 'c108',
191
- 'c103손익계산서q', 'c103재무상태표q', 'c103현금흐름표q',
192
- 'c103손익계산서y', 'c103재무상태표y', 'c103현금흐름표y',
193
- 'dart', 'etc')
194
-
195
- def __init__(self, client: MongoClient, code: str, page: str):
196
- if utils.is_6digit(code) and page in self.COLLECTIONS:
197
- super().__init__(client=client, db_name=code, col_name=page)
198
- else:
199
- raise Exception(f'Invalid value - code: {code} / {page}({self.COLLECTIONS})')
200
-
201
- @property
202
- def code(self):
203
- return self.db_name
204
-
205
- @code.setter
206
- def code(self, code: str):
207
- if utils.is_6digit(code):
208
- self.db_name = code
209
- else:
210
- raise Exception(f'Invalid value : {code}')
211
-
212
- @property
213
- def page(self):
214
- return self.col_name
215
-
216
- @page.setter
217
- def page(self, page: str):
218
- if page in self.COLLECTIONS:
219
- self.col_name = page
220
- else:
221
- raise Exception(f'Invalid value : {page}({self.COLLECTIONS})')
222
-
223
- # ========================End Properties=======================
224
-
225
- @staticmethod
226
- def latest_value(data: dict, pop_count=2, nan_to_zero=False, allow_empty=False) -> Tuple[str, float]:
227
- """
228
- 가장 최근 년/분기 값 - evaltools에서도 사용할수 있도록 staticmethod로 뺐음.
229
-
230
- 해당 타이틀의 가장 최근의 년/분기 값을 튜플 형식으로 반환한다.
231
-
232
- Args:
233
- data (dict): 찾고자하는 딕셔너리 데이터
234
- pop_count: 유효성 확인을 몇번할 것인가
235
- nan_to_zero: nan의 값을 0으로 바꿀것인가
236
- allow_empty: title 항목이 없을경우에도 에러를 발생시키지 않을 것인가
237
-
238
- Returns:
239
- tuple: ex - ('2020/09', 39617.5) or ('', 0)
240
-
241
- Note:
242
- 만약 최근 값이 nan 이면 찾은 값 바로 직전 것을 한번 더 찾아 본다.\n
243
- 데이터가 없는 경우 ('', nan) 반환한다.\n
244
- """
245
- def is_valid_value(value) -> bool:
246
- """
247
- 숫자가 아닌 문자열이나 nan 또는 None의 경우 유효한 형식이 아님을 알려 리턴한다.
248
- """
249
- if isinstance(value, str):
250
- # value : ('Unnamed: 1', '데이터가 없습니다.') 인 경우
251
- is_valid = False
252
- elif math.isnan(value):
253
- # value : float('nan') 인 경우
254
- is_valid = False
255
- elif value is None:
256
- # value : None 인 경우
257
- is_valid = False
258
- else:
259
- is_valid = True
260
- """
261
- elif value == 0:
262
- is_valid = False
263
- """
264
- return is_valid
265
-
266
- logger.debug(f'Corps.latest_value raw data : {data}')
267
-
268
- # 데이터를 추출해서 사용하기 때문에 원본 데이터는 보존하기 위해서 카피해서 사용
269
- data_copied = copy.deepcopy(data)
270
-
271
- for i in range(pop_count):
272
- try:
273
- d, v = data_copied.popitem()
274
- except KeyError:
275
- # when dictionary is empty
276
- return '', 0 if nan_to_zero else float('nan')
277
- if str(d).startswith('20') and is_valid_value(v):
278
- logger.debug(f'last_one : {v}')
279
- return d, v
280
-
281
- return '', 0 if nan_to_zero else float('nan')
282
-
283
- @staticmethod
284
- def refine_data(data: dict, refine_words: list) -> dict:
285
- """
286
- 주어진 딕셔너리에서 refine_words에 해당하는 키를 삭제해서 반환하는 유틸함수.
287
- c10346에서 사용
288
- refine_words : 정규표현식 가능
289
- """
290
- copy_data = data.copy()
291
- import re
292
- for regex_refine_word in refine_words:
293
- # refine_word에 해당하는지 정규표현식으로 검사하고 매치되면 삭제한다.
294
- p = re.compile(regex_refine_word)
295
- for title, _ in copy_data.items():
296
- # data 내부의 타이틀을 하나하나 조사한다.
297
- m = p.match(title)
298
- if m:
299
- del data[title]
300
- return data
301
-
302
- @staticmethod
303
- def get_all_codes(client: MongoClient) -> list:
304
- """
305
- 기업 코드를 데이터베이스명으로 가지는 모든 6자리 숫자 코드의 db 명 반환
306
- """
307
- corp_list = []
308
- for db in MongoBase.get_all_db_name(client):
309
- if utils.is_6digit(db):
310
- corp_list.append(db)
311
- return sorted(corp_list)
312
-
313
- @staticmethod
314
- def del_all_codes(client: MongoClient):
315
- corp_list = Corps.get_all_codes(client)
316
- for corp_db_name in corp_list:
317
- MongoBase.del_db(client, corp_db_name)
318
-
319
- @staticmethod
320
- def pick_rnd_x_code(client: MongoClient, count: int) -> list:
321
- """
322
- 임의의 갯수의 종목코드를 뽑아서 반환한다.
323
- """
324
- return random.sample(Corps.get_all_codes(client), count)
325
-
326
- @staticmethod
327
- def get_name(client: MongoClient, code: str):
328
- """
329
- code를 입력받아 종목명을 반환한다.
330
- """
331
- c101 = C101(client, code)
332
- try:
333
- name = c101.get_recent()['종목명']
334
- except KeyError:
335
- name = None
336
- return name
337
-
338
- def _save_df(self, df: pd.DataFrame) -> bool:
339
- # c103, c104, c106, c108에서 주로 사용하는 저장방식
340
- if df.empty:
341
- print('Dataframe is empty..So we will skip saving db..')
342
- return False
343
- result = self.my_col.insert_many(df.to_dict('records'))
344
- return result.acknowledged
345
-
346
- def _load_df(self) -> pd.DataFrame:
347
- # cdart와 c106, c103에서 주로 사용
348
- try:
349
- df = pd.DataFrame(self.get_all_docs())
350
- except KeyError:
351
- df = pd.DataFrame()
352
- return df
353
-
354
- def _save_dict(self, dict_data: dict, del_query: dict) -> bool:
355
- # c101, cdart에서 주로 사용하는 저장방식
356
- try:
357
- result = self.my_col.insert_one(dict_data)
358
- except errors.DuplicateKeyError:
359
- self.my_col.delete_many(del_query)
360
- result = self.my_col.insert_one(dict_data)
361
- return result.acknowledged
362
-
363
-
364
- class C1034(Corps, metaclass=ABCMeta):
365
- def __init__(self, client: MongoClient, db_name: str, col_name: str):
366
- super().__init__(client=client, code=db_name, page=col_name)
367
-
368
- def get_all_titles(self) -> list:
369
- titles = []
370
- for item in self.get_all_docs():
371
- titles.append(item['항목'])
372
- return list(set(titles))
373
-
374
- @staticmethod
375
- def sum_each_data(data_list: List[dict]) -> dict:
376
- """
377
- 검색된 딕셔너리를 모은 리스트를 인자로 받아서 각각의 기간에 맞춰 합을 구해 하나의 딕셔너리로 반환한다.
378
- """
379
- sum_dict = {}
380
- periods = list(data_list[0].keys())
381
- # 여러딕셔너리를 가진 리스트의 합 구하기
382
- for period in periods:
383
- sum_dict[period] = sum(utils.nan_to_zero(data[period]) for data in data_list)
384
- return sum_dict
385
-
386
- def _find(self, title: str, refine_words: list) -> Tuple[List[dict], int]:
387
- """
388
- refine_words 에 해당하는 딕셔너리 키를 삭제하고
389
- title 인자에 해당하는 항목을 검색하여 반환한다.
390
- c103의 경우는 중복되는 이름의 항목이 있기 때문에
391
- 이 함수는 반환되는 딕셔너리 리스트와 갯수로 구성되는 튜플을 반환한다.
392
- """
393
- titles = self.get_all_titles()
394
- if title in titles:
395
- count = 0
396
- data_list = []
397
- for data in self.my_col.find({'항목': {'$eq': title}}):
398
- # 도큐먼트에서 title과 일치하는 항목을 찾아낸다.
399
- count += 1
400
- # refine_data함수를 통해 삭제를 원하는 필드를 제거하고 data_list에 추가한다.
401
- data_list.append(self.refine_data(data, refine_words))
402
- return data_list, count
403
- else:
404
- raise Exception(f'{title} is not in {titles}')
405
-
406
- def latest_value(self, title: str, pop_count=2, nan_to_zero=False, allow_empty=False) -> Tuple[str, float]:
407
- od = OrderedDict(sorted(self.find(title, allow_empty=allow_empty).items(), reverse=False))
408
- logger.debug(f'{title} : {od}')
409
- return Corps.latest_value(od, pop_count, nan_to_zero, allow_empty)
410
-
411
- @abstractmethod
412
- def find(self, title: str, allow_empty=False) -> dict:
413
- pass
414
-
415
- def sum_recent_4q(self, title: str, nan_to_zero: bool = False) -> Tuple[str, float]:
416
- """최근 4분기 합
417
-
418
- 분기 페이지 한정 해당 타이틀의 최근 4분기의 합을 튜플 형식으로 반환한다.
419
-
420
- Args:
421
- title (str): 찾고자 하는 타이틀
422
- nan_to_zero: nan 값의 경우 zero로 반환한다.
423
-
424
- Returns:
425
- tuple: (계산된 4분기 중 최근분기, 총합)
426
-
427
- Raises:
428
- TypeError: 페이지가 q가 아닌 경우 발생
429
-
430
- Note:
431
- 분기 데이터가 4개 이하인 경우 그냥 최근 연도의 값을 찾아 반환한다.
432
- """
433
- if self.col_name.endswith('q'):
434
- # 딕셔너리 정렬 - https://kkamikoon.tistory.com/138
435
- # reverse = False 이면 오래된것부터 최근순으로 정렬한다.
436
- od_q = OrderedDict(sorted(self.find(title, allow_empty=True).items(), reverse=False))
437
- logger.debug(f'{title} : {od_q}')
438
-
439
- if len(od_q) < 4:
440
- # od_q의 값이 4개 이하이면 그냥 최근 연도의 값으로 반환한다.
441
- if self.page.startswith('c103'):
442
- y = C103(self.client, self.db_name, self.col_name[:-1] + 'y')
443
- elif self.page.startswith('c104'):
444
- y = C104(self.client, self.db_name, self.col_name[:-1] + 'y')
445
- else:
446
- Exception(f'Error on sum_recent_4q func...')
447
- return y.latest_value(title, nan_to_zero=nan_to_zero, allow_empty=True)
448
- else:
449
- q_sum = 0
450
- date_list = list(od_q.keys())
451
- while True:
452
- try:
453
- latest_period = date_list.pop()
454
- except IndexError:
455
- latest_period = ""
456
- break
457
- else:
458
- if str(latest_period).startswith('20'):
459
- break
460
-
461
- for i in range(4):
462
- # last = True 이면 최근의 값부터 꺼낸다.
463
- d, v = od_q.popitem(last=True)
464
- logger.debug(f'd:{d} v:{v}')
465
- q_sum += 0 if math.isnan(v) else v
466
- return str(latest_period), round(q_sum, 2)
467
- else:
468
- raise TypeError(f'Not support year data..{self.col_name}')
469
-
470
- def find_증감율(self, title: str) -> dict:
471
- """
472
-
473
- 타이틀에 해당하는 전년/분기대비 값을 반환한다.\n
474
-
475
- Args:
476
- title (str): 찾고자 하는 타이틀
477
-
478
- Returns:
479
- float: 전년/분기대비 증감율
480
-
481
- Note:
482
- 중복되는 title 은 취급하지 않기로함.\n
483
- """
484
- try:
485
- data_list, count = self._find(title, ['_id', '항목'])
486
- except:
487
- # title을 조회할 수 없는 경우
488
- if self.col_name.endswith('q'):
489
- r = {'전분기대비': math.nan}
490
- else:
491
- r = {'전년대비': math.nan, '전년대비 1': math.nan}
492
- return r
493
- logger.info(data_list)
494
- cmp_dict = {}
495
- if count > 1:
496
- # 중복된 타이틀을 가지는 페이지의 경우 경고 메시지를 보낸다.
497
- logger.warning(f'Not single data..{self.code}/{self.page}/{title}')
498
- logger.warning(data_list)
499
- # 첫번째 데이터를 사용한다.
500
- data_dict = data_list[0]
501
- for k, v in data_dict.items():
502
- if str(k).startswith('전'):
503
- cmp_dict[k] = v
504
- return cmp_dict
505
-
506
-
507
- class C104(C1034):
508
- def __init__(self, client: MongoClient, code: str, page: str):
509
- if page in ('c104y', 'c104q'):
510
- super().__init__(client=client, db_name=code, col_name=page)
511
- else:
512
- raise Exception
513
-
514
- def get_all_titles(self) -> list:
515
- """
516
- 상위 C1034클래스에서 c104는 stamp항목이 있기 때문에 삭제하고 리스트로 반환한다.
517
- """
518
- titles = super().get_all_titles()
519
- titles.remove('stamp')
520
- return titles
521
-
522
- def find(self, title: str, allow_empty=False) -> dict:
523
- """
524
- title에 해당하는 항목을 딕셔너리로 반환한다.
525
- allow_empty를 true로 하면 에러를 발생시키지 않고 빈딕셔너리를 리턴한다.
526
- """
527
- try:
528
- l, c = super(C104, self)._find(title, ['_id', '항목', '^전.+대비.*'])
529
- return_dict = l[0]
530
- except Exception as e:
531
- if allow_empty:
532
- return_dict = {}
533
- else:
534
- raise Exception(e)
535
- return return_dict
536
-
537
- def save_df(self, c104_df: pd.DataFrame) -> bool:
538
- """데이터베이스에 저장
539
-
540
- c104는 4페이지의 자료를 한 컬렉션에 모으는 것이기 때문에
541
- stamp 를 검사하여 12시간 전보다 이전에 저장된 자료가 있으면
542
- 삭제한 후 저장하고 12시간 이내의 자료는 삭제하지 않고
543
- 데이터를 추가하는 형식으로 저장한다.
544
-
545
- Example:
546
- c104_data 예시\n
547
- [{'항목': '매출액증가율',...'2020/12': 2.78, '2021/12': 14.9, '전년대비': 8.27, '전년대비1': 12.12},
548
- {'항목': '영업이익증가율',...'2020/12': 29.62, '2021/12': 43.86, '전년대비': 82.47, '전년대비1': 14.24}]
549
-
550
- Note:
551
- 항목이 중복되는 경우가 있기 때문에 c104처럼 각 항목을 키로하는 딕셔너리로 만들지 않는다.
552
- """
553
- self.my_col.create_index('항목', unique=True)
554
- time_now = datetime.datetime.now()
555
- try:
556
- stamp = self.my_col.find_one({'항목': 'stamp'})['time']
557
- if stamp < (time_now - datetime.timedelta(days=.01)):
558
- # 스템프가 약 10분 이전이라면..연속데이터가 아니라는 뜻이므로 컬렉션을 초기화한다.
559
- print("Before save data, cleaning the collection...", end='')
560
- self.clear_docs_in_col()
561
- except TypeError:
562
- # 스템프가 없다면...
563
- pass
564
- # 항목 stamp를 찾아 time을 업데이트하고 stamp가 없으면 insert한다.
565
- self.my_col.update_one({'항목': 'stamp'}, {"$set": {'time': time_now}}, upsert=True)
566
- return super(C104, self)._save_df(c104_df)
567
-
568
- def get_stamp(self) -> datetime.datetime:
569
- """
570
- c104y, c104q가 작성된 시간이 기록된 stamp 항목을 datetime 형식으로 리턴한다.
571
- """
572
- return self.my_col.find_one({"항목": "stamp"})['time']
573
-
574
- def modify_stamp(self, days_ago: int):
575
- """
576
- 인위적으로 타임스템프를 수정한다 - 주로 테스트 용도
577
- """
578
- try:
579
- before = self.my_col.find_one({'항목': 'stamp'})['time']
580
- except TypeError:
581
- # 이전에 타임스템프가 없는 경우
582
- before = None
583
- time_2da = datetime.datetime.now() - datetime.timedelta(days=days_ago)
584
- self.my_col.update_one({'항목': 'stamp'}, {"$set": {'time': time_2da}}, upsert=True)
585
- after = self.my_col.find_one({'항목': 'stamp'})['time']
586
- logger.info(f"Stamp changed: {before} -> {after}")
587
-
588
-
589
- class C103(C1034):
590
- def __init__(self, client: MongoClient, code: str, page: str):
591
- if page in ('c103손익계산서q', 'c103재무상태표q', 'c103현금흐름표q',
592
- 'c103손익계산서y', 'c103재무상태표y', 'c103현금흐름표y',):
593
- super().__init__(client=client, db_name=code, col_name=page)
594
- else:
595
- raise Exception
596
-
597
- def save_df(self, c103_df: pd.DataFrame) -> bool:
598
- """데이터베이스에 저장
599
-
600
- Example:
601
- c103_list 예시\n
602
- [{'항목': '자산총계', '2020/03': 3574575.4, ... '전분기대비': 3.9},
603
- {'항목': '유동자산', '2020/03': 1867397.5, ... '전분기대비': 5.5}]
604
-
605
- Note:
606
- 항목이 중복되는 경우가 있기 때문에 c104처럼 각 항목을 키로하는 딕셔너리로 만들지 않는다.
607
- """
608
- self.my_col.create_index('항목', unique=False)
609
- print("Before save data, cleaning the collection...", end='')
610
- self.clear_docs_in_col()
611
- return super(C103, self)._save_df(c103_df)
612
-
613
- def load_df(self) -> pd.DataFrame:
614
- """
615
- 데이터베이스에 저장된 페이지를 데이터프레임으로 반환한다.
616
- """
617
- return super(C103, self)._load_df()
618
-
619
- def find(self, title: str, allow_empty=False) -> dict:
620
- """
621
- title에 해당하는 항목을 딕셔너리로 반환한다.
622
- allow_empty를 true로 하면 에러를 발생시키지 않고 빈딕셔너리를 리턴한다.
623
- c103의 경우는 중복된 타이틀을 가지는 항목이 있어 합쳐서 한개의 딕셔너리로 반환한다.
624
- """
625
- try:
626
- l, c = super(C103, self)._find(title, ['_id', '항목', '^전.+대비.*'])
627
- sum_dict = self.sum_each_data(l)
628
- except Exception as e:
629
- if allow_empty:
630
- sum_dict = {}
631
- else:
632
- raise Exception(e)
633
- return sum_dict
634
-
635
-
636
- class C101(Corps):
637
- def __init__(self, client: MongoClient, code: str):
638
- super().__init__(client=client, code=code, page='c101')
639
- self.my_col.create_index('date', unique=True)
640
-
641
- def save_dict(self, c101_dict: dict) -> bool:
642
- """
643
- c101의 구조에 맞는 딕셔너리값을 받아서 구조가 맞는지 확인하고 맞으면 저장한다.
644
-
645
- Note:
646
- <c101_struc>\n
647
- 'date', '코드', '종목명',\n
648
- '업종', '주가', '거래량',\n
649
- 'EPS', 'BPS', 'PER',\n
650
- '업종PER', 'PBR', '배당수익률',\n
651
- '최고52주', '최저52주', '거래대금',\n
652
- '시가총액', '베타52주', '발행주식',\n
653
- '유통비율', 'intro'\n
654
- """
655
- c101_template = ['date', '코드', '종목명', '업종', '주가', '거래량', 'EPS', 'BPS', 'PER', '업종PER', 'PBR',
656
- '배당수익률', '최고52주', '최저52주', '거래대금', '시가총액', '베타52주', '발행주식', '유통비율']
657
- # 리스트 비교하기
658
- # reference from https://codetorial.net/tips_and_examples/compare_two_lists.html
659
- if c101_dict['코드'] != self.db_name:
660
- raise Exception("Code isn't equal input data and db data..")
661
- logger.debug(c101_dict.keys())
662
- # c101 데이터가 c101_template의 내용을 포함하는가 확인하는 if문
663
- # refered from https://appia.tistory.com/101
664
- if (set(c101_template) - set(c101_dict.keys())) == set():
665
- # 스크랩한 날짜 이후의 데이터는 조회해서 먼저 삭제한다.
666
- del_query = {'date': {"$gte": c101_dict['date']}}
667
- return super(C101, self)._save_dict(c101_dict, del_query)
668
- else:
669
- raise Exception('Invalid c101 dictionary structure..')
670
-
671
- def find(self, date: str) -> dict:
672
- """
673
-
674
- 해당 날짜의 데이터를 반환한다.
675
- 만약 리턴값이 없으면 {} 을 반환한다.
676
-
677
- Args:
678
- date (str): 예 - 20201011(6자리숫자)
679
- """
680
- if utils.isYmd(date):
681
- converted_date = date[:4] + '.' + date[4:6] + '.' + date[6:]
682
- else:
683
- raise Exception(f'Invalid date format : {date}(ex-20201011(8자리숫자))')
684
- d = self.my_col.find_one({'date': converted_date})
685
- if d is None:
686
- return {}
687
- else:
688
- del d['_id']
689
- return d
690
-
691
- def get_recent(self, merge_intro=False) -> dict:
692
- """
693
- 저장된 데이터에서 가장 최근 날짜의 딕셔너리를 반환한다.
694
-
695
- 리턴값
696
- {'BPS': 50817.0,
697
- 'EPS': 8057.0,
698
- 'PBR': 1.28,
699
- 'PER': 8.08,
700
- 'date': '2023.04.14',
701
- 'intro1': '한국 및 DX부문 해외 9개 지역총괄과 DS부문 해외 5개 지역총괄, SDC, Harman 등 233개의 종속기업으로 구성된 글로벌 전자기업임.',
702
- 'intro2': '세트사업(DX)에는 TV, 냉장고 등을 생산하는 CE부문과 스마트폰, 네트워크시스템, 컴퓨터 등을 생산하는 IM부문이 있음.',
703
- 'intro3': '부품사업(DS)에서는 D램, 낸드 플래쉬, 모바일AP 등의 제품을 생산하는 반도체 사업과 TFT-LCD 및 OLED 디스플레이 패널을 생산하는 DP사업으로 구성됨.',
704
- '거래대금': '1062800000000',
705
- '거래량': '16176500',
706
- '발행주식': '5969782550',
707
- '배당수익률': '2.22',
708
- '베타52주': '0.95',
709
- '시가총액': '388632800000000',
710
- '업종': '반도체와반도체장비',
711
- '업종PER': '8.36',
712
- '유통비율': '75.82',
713
- '종목명': '삼성전자',
714
- '주가': '65100',
715
- '최고52주': '68800',
716
- '최저52주': '51800',
717
- '코드': '005930'}
718
- """
719
- try:
720
- d = self.my_col.find({'date': {'$exists': True}}, {"_id": 0}).sort('date', pymongo.DESCENDING).next()
721
- # del doc['_id'] - 위의 {"_id": 0} 으로 해결됨.
722
- if merge_intro:
723
- d['intro'] = d['intro1'] + d['intro2'] + d['intro3']
724
- del d['intro1']
725
- del d['intro2']
726
- del d['intro3']
727
- except StopIteration:
728
- d = {}
729
- return d
730
-
731
- def get_all(self) -> list:
732
- """
733
-
734
- 저장된 모든 데이터를 딕셔너리로 가져와서 리스트로 포장하여 반환한다.
735
- """
736
- items = []
737
- for doc in self.my_col.find({'date': {'$exists': True}}, {"_id": 0}).sort('date', pymongo.ASCENDING):
738
- # del doc['_id'] - 위의 {"_id": 0} 으로 해결됨.
739
- items.append(doc)
740
- return items
741
-
742
- def get_trend(self, title: str) -> dict:
743
- """
744
- title에 해당하는 데이터베이스에 저장된 모든 값을 {날짜: 값} 형식의 딕셔너리로 반환한다.
745
-
746
- title should be in ['BPS', 'EPS', 'PBR', 'PER', '주가', '배당수익률', '베타52주', '거래량']
747
-
748
- 리턴값 - 주가
749
- {'2023.04.05': '63900',
750
- '2023.04.06': '62300',
751
- '2023.04.07': '65000',
752
- '2023.04.10': '65700',
753
- '2023.04.11': '65900',
754
- '2023.04.12': '66000',
755
- '2023.04.13': '66100',
756
- '2023.04.14': '65100',
757
- '2023.04.17': '65300'}
758
- """
759
- titles = ['BPS', 'EPS', 'PBR', 'PER', '주가', '배당수익률', '베타52주', '거래량']
760
- if title not in titles:
761
- raise Exception(f"title should be in {titles}")
762
- items = dict()
763
- for doc in self.my_col.find({'date': {'$exists': True}}, {"_id": 0, "date": 1, f"{title}": 1}).sort('date', pymongo.ASCENDING):
764
- items[doc['date']] = doc[f'{title}']
765
- return items
766
-
767
- class C106(Corps):
768
- def __init__(self, client: MongoClient, code: str, page: str):
769
- if page in ('c106y', 'c106q'):
770
- super().__init__(client=client, code=code, page=page)
771
- else:
772
- raise Exception
773
-
774
- def save_df(self, c106_df: pd.DataFrame) -> bool:
775
- self.my_col.create_index('항목', unique=True)
776
- self.clear_docs_in_col()
777
- return super(C106, self)._save_df(c106_df)
778
-
779
- def load_df(self) -> pd.DataFrame:
780
- return super(C106, self)._load_df()
781
-
782
- def get_all_titles(self) -> list:
783
- titles = []
784
- for item in self.get_all_docs():
785
- titles.append(item['항목'])
786
- return list(set(titles))
787
-
788
- def find(self, title: str, allow_empty=False) -> dict:
789
- """
790
- title에 해당하는 항목을 딕셔너리로 반환한다.
791
- """
792
- titles = self.get_all_titles()
793
- if title in titles:
794
- data = self.my_col.find_one({'항목': {'$eq': title}})
795
- return self.refine_data(data, ['_id', '항목'])
796
- else:
797
- if allow_empty:
798
- return {}
799
- else:
800
- raise Exception(f'{title} is not in {titles}')
801
-
802
-
803
- class DateBase(MongoBase):
804
- """
805
- 날짜를 컬렉션으로 가지는 데이터베이스를 위한 기반클래스
806
- """
807
- def __init__(self, client: MongoClient, db_name: str, date: str):
808
- if utils.isYmd(date):
809
- super().__init__(client=client, db_name=db_name, col_name=date)
810
- else:
811
- raise Exception(f"Invalid date : {date}(%Y%m%d)")
812
-
813
- @property
814
- def date(self):
815
- return self.col_name
816
-
817
- @date.setter
818
- def date(self, date: str):
819
- if utils.isYmd(date):
820
- self.col_name = date
821
- else:
822
- raise Exception(f"Invalid date : {date}(%Y%m%d)")
823
-
824
- # ========================End Properties=======================
825
-
826
- def save_df(self, df: pd.DataFrame) -> bool:
827
- if df.empty:
828
- print('Dataframe is empty..So we will skip saving db..')
829
- return False
830
-
831
- self.clear_docs_in_col()
832
- print(f"Save new data to '{self.db_name}' / '{self.col_name}'")
833
- result = self.my_col.insert_many(df.to_dict('records'))
834
- return result.acknowledged
835
-
836
- def load_df(self) -> pd.DataFrame:
837
- try:
838
- df = pd.DataFrame(list(self.my_col.find({}))).drop(columns=['_id'])
839
- except KeyError:
840
- df = pd.DataFrame()
841
- return df
842
-
843
-
844
- class EvalByDate(DateBase):
845
- """
846
- 각 날짜별로 만들어진 eval-report 데이터프레임을 관리하는 클래스
847
- DB_NAME : eval
848
- COL_NAME : Ymd형식 날짜
849
- """
850
- EVAL_DB = 'eval'
851
-
852
- def __init__(self, client: MongoClient, date: str):
853
- super().__init__(client, self.EVAL_DB, date)
854
- # 인덱스 설정
855
- self.my_col.create_index('code', unique=True)
856
-
857
- @staticmethod
858
- def get_dates(client: MongoClient) -> List[str]:
859
- # 데이터베이스에 저장된 날짜 목록을 리스트로 반환한다.
860
- dates_list = client.eval.list_collection_names()
861
- dates_list.sort()
862
- return dates_list
863
-
864
- @classmethod
865
- def get_recent(cls, client: MongoClient, type: str):
866
- """
867
- eval 데이터베이스의 가장 최근의 유요한 자료를 반환한다.
868
- type의 종류에 따라 반환값이 달라진다.[date, dataframe, dict]
869
- """
870
- dates = cls.get_dates(client)
871
-
872
- while len(dates) > 0:
873
- recent_date = dates.pop()
874
- recent_df = cls(client, recent_date).load_df()
875
- if len(recent_df) != 0:
876
- if type == 'date':
877
- return recent_date
878
- elif type == 'dataframe':
879
- return recent_df
880
- elif type == 'dict':
881
- return recent_df.to_dict('records')
882
- else:
883
- raise Exception(f"Invalid type : {type}")
884
-
885
- return None
886
-
887
- class MI(MongoBase):
888
- """mi 데이터베이스 클래스
889
-
890
- Note:
891
- db - mi\n
892
- col - 'aud', 'chf', 'gbond3y', 'gold', 'silver', 'kosdaq', 'kospi', 'sp500', 'usdkrw', 'wti', 'avgper', 'yieldgap', 'usdidx' - 총 13개\n
893
- doc - date, value\n
894
- """
895
- MI_DB = 'mi'
896
- COL_TITLE = ('aud', 'chf', 'gbond3y', 'gold', 'silver', 'kosdaq', 'kospi',
897
- 'sp500', 'usdkrw', 'wti', 'avgper', 'yieldgap', 'usdidx')
898
-
899
- def __init__(self, client: MongoClient, index: str):
900
- if index in self.COL_TITLE:
901
- super().__init__(client=client, db_name=self.MI_DB, col_name=index)
902
- else:
903
- raise Exception(f'Invalid index : {index}({self.COL_TITLE})')
904
-
905
- @property
906
- def index(self):
907
- return self.col_name
908
-
909
- @index.setter
910
- def index(self, index: str):
911
- if index in self.COL_TITLE:
912
- self.col_name = index
913
- else:
914
- raise Exception(f'Invalid index : {index}({self.COL_TITLE})')
915
-
916
- # ========================End Properties=======================
917
-
918
- def get_recent(self) -> Tuple[str, float]:
919
- """저장된 가장 최근의 값을 반환하는 함수
920
- """
921
- if self.validate_col():
922
- d = self.my_col.find({'date': {'$exists': True}}).sort('date', pymongo.DESCENDING).next()
923
- del d['_id']
924
- return d['date'], d['value']
925
-
926
- def save_dict(self, mi_dict: dict) -> bool:
927
- """MI 데이터 저장
928
-
929
- Args:
930
- mi_dict (dict): ex - {'date': '2021.07.21', 'value': '1154.50'}
931
- """
932
- self.my_col.create_index('date', unique=True)
933
- result = self.my_col.update_one({'date': mi_dict['date']}, {"$set": {'value': mi_dict['value']}}, upsert=True)
934
- return result.acknowledged