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
@@ -0,0 +1,934 @@
|
|
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
|