db2_hj3415 0.1.7__tar.gz → 0.1.9__tar.gz
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.
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/PKG-INFO +1 -2
- db2_hj3415-0.1.9/db2_hj3415/common/connection.py +36 -0
- db2_hj3415-0.1.9/db2_hj3415/common/utils.py +25 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/__init__.py +1 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/_c10346.py +85 -15
- db2_hj3415-0.1.9/db2_hj3415/nfs/_ops.py +82 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/c101.py +9 -1
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/c103.py +14 -2
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/c104.py +14 -2
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/c106.py +14 -2
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/models.py +7 -2
- db2_hj3415-0.1.9/db2_hj3415/valuation/__init__.py +9 -0
- db2_hj3415-0.1.9/db2_hj3415/valuation/_ops.py +65 -0
- db2_hj3415-0.1.9/db2_hj3415/valuation/blue.py +17 -0
- db2_hj3415-0.1.9/db2_hj3415/valuation/growth.py +17 -0
- db2_hj3415-0.1.9/db2_hj3415/valuation/mil.py +17 -0
- db2_hj3415-0.1.9/db2_hj3415/valuation/models.py +193 -0
- db2_hj3415-0.1.9/db2_hj3415/valuation/red.py +17 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/pyproject.toml +1 -2
- db2_hj3415-0.1.7/db2_hj3415/common/connection.py +0 -29
- db2_hj3415-0.1.7/db2_hj3415/common/utils.py +0 -24
- db2_hj3415-0.1.7/db2_hj3415/nfs/_ops.py +0 -42
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/README.md +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/__init__.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/cli/__init__.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/cli/db.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/common/__init__.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/common/db_ops.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/__init__.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/_ops.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/aud.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/chf.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/gbond3y.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/gold.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/kosdaq.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/kospi.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/silver.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/sp500.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/usdidx.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/usdkrw.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/mi/wti.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/c108.py +0 -0
- {db2_hj3415-0.1.7 → db2_hj3415-0.1.9}/db2_hj3415/nfs/dart.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: db2_hj3415
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.9
|
4
4
|
Summary: Gathering the stock data by playwright
|
5
5
|
Author-email: Hyungjin Kim <hj3415@gmail.com>
|
6
6
|
Description-Content-Type: text/markdown
|
@@ -10,7 +10,6 @@ Requires-Dist: pandas
|
|
10
10
|
Requires-Dist: pandas-stubs
|
11
11
|
Requires-Dist: deepdiff
|
12
12
|
Requires-Dist: utils_hj3415>=3.2.3
|
13
|
-
Requires-Dist: redis
|
14
13
|
Requires-Dist: pydantic
|
15
14
|
Project-URL: Home, https://www.hyungjin.kr
|
16
15
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# MongoDB 연결
|
2
|
+
import os
|
3
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
4
|
+
|
5
|
+
# 싱글톤 몽고 클라이언트 정의
|
6
|
+
MONGO_URI = os.getenv("MONGO_ADDR", "mongodb://localhost:27017")
|
7
|
+
client: AsyncIOMotorClient = None
|
8
|
+
|
9
|
+
def get_mongo_client() -> AsyncIOMotorClient:
|
10
|
+
global client
|
11
|
+
if client is None:
|
12
|
+
client = AsyncIOMotorClient(MONGO_URI)
|
13
|
+
return client
|
14
|
+
|
15
|
+
def close_mongo_client():
|
16
|
+
if client:
|
17
|
+
client.close()
|
18
|
+
|
19
|
+
|
20
|
+
from pymongo import MongoClient
|
21
|
+
|
22
|
+
client_sync: MongoClient = None # 동기 클라이언트 타입으로 변경
|
23
|
+
|
24
|
+
def get_mongo_client_sync() -> MongoClient:
|
25
|
+
"""
|
26
|
+
MongoDB 동기 클라이언트를 반환합니다.
|
27
|
+
전역 client가 None일 경우 새 클라이언트를 생성합니다.
|
28
|
+
"""
|
29
|
+
global client_sync
|
30
|
+
if client_sync is None:
|
31
|
+
client_sync = MongoClient(MONGO_URI)
|
32
|
+
return client_sync
|
33
|
+
|
34
|
+
def close_mongo_client_sync():
|
35
|
+
if client_sync:
|
36
|
+
client_sync.close()
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# 자주 쓰는 간단한 유틸 함수
|
2
|
+
import pandas as pd
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
def df_to_dict_replace_nan(df: pd.DataFrame) -> list[dict]:
|
6
|
+
# NaN → None으로 변환
|
7
|
+
return df.replace({np.nan: None}).to_dict(orient="records")
|
8
|
+
|
9
|
+
import json
|
10
|
+
from bson import json_util
|
11
|
+
from pydantic import BaseModel
|
12
|
+
|
13
|
+
def pretty_print(obj):
|
14
|
+
def convert(o):
|
15
|
+
if isinstance(o, BaseModel):
|
16
|
+
return o.model_dump(by_alias=True)
|
17
|
+
if isinstance(o, dict):
|
18
|
+
return {k: convert(v) for k, v in o.items()}
|
19
|
+
if isinstance(o, list):
|
20
|
+
return [convert(v) for v in o]
|
21
|
+
return o # 기본값 (예: str, int, float 등)
|
22
|
+
|
23
|
+
data = convert(obj)
|
24
|
+
|
25
|
+
print(json.dumps(data, indent=2, ensure_ascii=False, default=json_util.default))
|
@@ -62,7 +62,7 @@ async def _compare_and_log_diff(code: str, new_doc: dict, latest_doc: dict | Non
|
|
62
62
|
return True
|
63
63
|
|
64
64
|
|
65
|
-
def
|
65
|
+
def _prepare_c10346_document_for_save(code: str, data: dict[str, pd.DataFrame]) -> dict:
|
66
66
|
now = datetime.now(timezone.utc)
|
67
67
|
document = {"코드": code, "날짜": now}
|
68
68
|
|
@@ -80,7 +80,7 @@ async def save(col: str, code: str, data: dict[str, pd.DataFrame], client: Async
|
|
80
80
|
|
81
81
|
await collection.create_index([("코드", ASCENDING), ("날짜", ASCENDING)], unique=True)
|
82
82
|
|
83
|
-
document =
|
83
|
+
document = _prepare_c10346_document_for_save(code, data)
|
84
84
|
latest_doc = await collection.find_one({"코드": code}, sort=[("날짜", DESCENDING)])
|
85
85
|
|
86
86
|
need_save = await _compare_and_log_diff(code, document, latest_doc, client)
|
@@ -106,7 +106,7 @@ async def save_many(col: str, many_data: dict[str, dict[str, pd.DataFrame]], cli
|
|
106
106
|
results = []
|
107
107
|
|
108
108
|
for code, data in many_data.items():
|
109
|
-
document =
|
109
|
+
document = _prepare_c10346_document_for_save(code, data)
|
110
110
|
latest_doc = await collection.find_one({"코드": code}, sort=[("날짜", DESCENDING)])
|
111
111
|
need_save = await _compare_and_log_diff(code, document, latest_doc, client)
|
112
112
|
|
@@ -127,7 +127,7 @@ async def save_many(col: str, many_data: dict[str, dict[str, pd.DataFrame]], cli
|
|
127
127
|
return results
|
128
128
|
|
129
129
|
|
130
|
-
async def
|
130
|
+
async def fetch_latest_doc(col: str, code: str, client: AsyncIOMotorClient) -> dict | None:
|
131
131
|
collection = get_collection(client, DB_NAME, col)
|
132
132
|
|
133
133
|
# 최신 날짜 기준으로 정렬하여 1건만 조회
|
@@ -142,19 +142,89 @@ async def get_latest(col: str, code: str, client: AsyncIOMotorClient) -> C103 |
|
|
142
142
|
|
143
143
|
latest_doc["_id"] = str(latest_doc["_id"])
|
144
144
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
return C106(**latest_doc)
|
153
|
-
|
154
|
-
except Exception as e:
|
155
|
-
print(f"[{code}] C103 파싱 실패: {e}")
|
145
|
+
return latest_doc
|
146
|
+
|
147
|
+
|
148
|
+
async def get_latest_as_model(col: str, code: str, client: AsyncIOMotorClient) -> C103 | C104 | C106 | None:
|
149
|
+
latest_doc = await fetch_latest_doc(col, code, client)
|
150
|
+
|
151
|
+
if not latest_doc:
|
156
152
|
return None
|
157
153
|
|
154
|
+
match col:
|
155
|
+
case 'c103':
|
156
|
+
return C103(**latest_doc)
|
157
|
+
case 'c104':
|
158
|
+
return C104(**latest_doc)
|
159
|
+
case 'c106':
|
160
|
+
return C106(**latest_doc)
|
161
|
+
case _:
|
162
|
+
raise ValueError(f"지원하지 않는 컬렉션 이름: {col}")
|
163
|
+
|
164
|
+
|
165
|
+
async def get_latest_doc_as_df_dict(col: str, code: str, client: AsyncIOMotorClient) -> dict[str, pd.DataFrame] | None:
|
166
|
+
latest_doc = await fetch_latest_doc(col, code, client)
|
167
|
+
|
168
|
+
if not latest_doc:
|
169
|
+
return None
|
170
|
+
|
171
|
+
keys_map = {
|
172
|
+
'c103': [
|
173
|
+
"손익계산서y", "재무상태표y", "현금흐름표y",
|
174
|
+
"손익계산서q", "재무상태표q", "현금흐름표q"
|
175
|
+
],
|
176
|
+
'c104': [
|
177
|
+
'수익성y', '성장성y', '안정성y', '활동성y', '가치분석y',
|
178
|
+
'수익성q', '성장성q', '안정성q', '활동성q', '가치분석q'
|
179
|
+
],
|
180
|
+
'c106': ['q', 'y']
|
181
|
+
}
|
182
|
+
|
183
|
+
def make_df_dict(keys: list[str]) -> dict[str, pd.DataFrame]:
|
184
|
+
"""latest_doc에서 주어진 키들을 기반으로 DataFrame 생성"""
|
185
|
+
return {
|
186
|
+
key: pd.DataFrame(latest_doc.get(key) or []) for key in keys
|
187
|
+
}
|
188
|
+
|
189
|
+
def concat_by_suffix(dfs: dict[str, pd.DataFrame]) -> dict[str, pd.DataFrame]:
|
190
|
+
"""'q', 'y' 접미사 기준으로 그룹 후 하나의 DataFrame으로 합침"""
|
191
|
+
grouped: dict[str, list[pd.DataFrame]] = {'q': [], 'y': []}
|
192
|
+
|
193
|
+
for key, df in dfs.items():
|
194
|
+
if key.endswith('q'):
|
195
|
+
grouped['q'].append(df.assign(분류=key))
|
196
|
+
elif key.endswith('y'):
|
197
|
+
grouped['y'].append(df.assign(분류=key))
|
198
|
+
|
199
|
+
def clean_df(df: pd.DataFrame) -> pd.DataFrame:
|
200
|
+
# 모든 값이 NaN인 열 제거
|
201
|
+
return df.dropna(axis=1, how='all')
|
202
|
+
|
203
|
+
return {
|
204
|
+
period: (
|
205
|
+
pd.concat(
|
206
|
+
[clean_df(f) for f in frames if not f.empty],
|
207
|
+
ignore_index=True
|
208
|
+
) if any(not f.empty for f in frames) else pd.DataFrame()
|
209
|
+
)
|
210
|
+
for period, frames in grouped.items()
|
211
|
+
}
|
212
|
+
|
213
|
+
# 2. 실제 처리 로직
|
214
|
+
match col:
|
215
|
+
case 'c103':
|
216
|
+
return make_df_dict(keys_map['c103'])
|
217
|
+
|
218
|
+
case 'c104':
|
219
|
+
raw_result = make_df_dict(keys_map['c104'])
|
220
|
+
return concat_by_suffix(raw_result)
|
221
|
+
|
222
|
+
case 'c106':
|
223
|
+
return make_df_dict(keys_map['c106'])
|
224
|
+
|
225
|
+
case _:
|
226
|
+
raise ValueError(f"지원하지 않는 컬렉션 이름: {col}")
|
227
|
+
|
158
228
|
|
159
229
|
async def has_doc_changed(col: str, code: str, client: AsyncIOMotorClient) -> bool:
|
160
230
|
"""
|
@@ -0,0 +1,82 @@
|
|
1
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
2
|
+
from db2_hj3415.nfs import DB_NAME, connection
|
3
|
+
from db2_hj3415.nfs.models import CodeName
|
4
|
+
|
5
|
+
|
6
|
+
async def get_all_codes(client: AsyncIOMotorClient) -> list[str]:
|
7
|
+
"""
|
8
|
+
c103, c104, c106 컬렉션에 모두 존재하는 코드의 리스트를 반환함.
|
9
|
+
|
10
|
+
Args:
|
11
|
+
client (AsyncIOMotorClient): MongoDB 비동기 클라이언트 객체
|
12
|
+
|
13
|
+
Returns:
|
14
|
+
list[str]: c103, c104, c106 컬렉션에 공통으로 존재하는 종목 코드 리스트
|
15
|
+
"""
|
16
|
+
db = client[DB_NAME]
|
17
|
+
|
18
|
+
collections = ['c103', 'c104', 'c106']
|
19
|
+
|
20
|
+
# 첫 컬렉션으로 초기화
|
21
|
+
s = set(await db[collections[0]].distinct("코드"))
|
22
|
+
|
23
|
+
for col in collections[1:]:
|
24
|
+
codes = await db[col].distinct("코드")
|
25
|
+
s &= set(codes)
|
26
|
+
|
27
|
+
return list(s)
|
28
|
+
|
29
|
+
|
30
|
+
def get_all_codes_sync() -> list[str]:
|
31
|
+
"""
|
32
|
+
c103, c104, c106 컬렉션에 모두 존재하는 코드의 리스트를 반환함.
|
33
|
+
"""
|
34
|
+
client = connection.get_mongo_client_sync()
|
35
|
+
try:
|
36
|
+
db = client[DB_NAME]
|
37
|
+
collections = ['c103', 'c104', 'c106']
|
38
|
+
|
39
|
+
# 첫 컬렉션 코드 셋팅
|
40
|
+
common_codes = set(db[collections[0]].distinct("코드"))
|
41
|
+
|
42
|
+
for col in collections[1:]:
|
43
|
+
codes = db[col].distinct("코드")
|
44
|
+
common_codes &= set(codes)
|
45
|
+
|
46
|
+
return sorted(common_codes) # 필요에 따라 정렬
|
47
|
+
finally:
|
48
|
+
connection.close_mongo_client_sync()
|
49
|
+
|
50
|
+
|
51
|
+
async def get_all_codes_names(client: AsyncIOMotorClient) -> list[CodeName]:
|
52
|
+
collection = client[DB_NAME]['c101']
|
53
|
+
cursor = collection.find({}, {"코드": 1, "종목명": 1, "_id": 0})
|
54
|
+
result = []
|
55
|
+
async for doc in cursor:
|
56
|
+
result.append(CodeName(**doc))
|
57
|
+
return result
|
58
|
+
|
59
|
+
|
60
|
+
def get_all_codes_names_sync() -> list[CodeName] | None:
|
61
|
+
client = connection.get_mongo_client_sync()
|
62
|
+
try:
|
63
|
+
collection = client[DB_NAME]['c101']
|
64
|
+
cursor = collection.find({}, {"코드": 1, "종목명": 1, "_id": 0})
|
65
|
+
return [CodeName(**doc) for doc in cursor]
|
66
|
+
finally:
|
67
|
+
connection.close_mongo_client_sync()
|
68
|
+
|
69
|
+
async def delete_code_from_all_collections(code: str, client: AsyncIOMotorClient) -> dict[str, int]:
|
70
|
+
db = client[DB_NAME]
|
71
|
+
|
72
|
+
collections = ['c101', 'c103', 'c104', 'c106', 'c108']
|
73
|
+
|
74
|
+
deleted_counts = {}
|
75
|
+
|
76
|
+
for col in collections:
|
77
|
+
result = await db[col].delete_many({"코드": code})
|
78
|
+
deleted_counts[col] = result.deleted_count
|
79
|
+
|
80
|
+
print(f"삭제된 도큐먼트 갯수: {deleted_counts}")
|
81
|
+
return deleted_counts
|
82
|
+
|
@@ -104,10 +104,18 @@ async def get_latest(code: str, client: AsyncIOMotorClient) -> C101 | None:
|
|
104
104
|
mylogger.debug(doc)
|
105
105
|
return C101(**doc)
|
106
106
|
else:
|
107
|
-
|
107
|
+
mylogger.warning(f"데이터 없음: {code}")
|
108
108
|
return None
|
109
109
|
|
110
110
|
|
111
|
+
async def get_name(code: str, client: AsyncIOMotorClient) -> str | None:
|
112
|
+
c101_data = await get_latest(code, client)
|
113
|
+
if c101_data is None:
|
114
|
+
return None
|
115
|
+
else:
|
116
|
+
return c101_data.종목명
|
117
|
+
|
118
|
+
|
111
119
|
SortOrder = Literal["asc", "desc"]
|
112
120
|
|
113
121
|
async def get_all_data(code: str, client: AsyncIOMotorClient, sort: SortOrder = 'asc') -> list[C101]:
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
1
3
|
from motor.motor_asyncio import AsyncIOMotorClient
|
2
4
|
import pandas as pd
|
3
5
|
|
@@ -17,8 +19,18 @@ async def save_many(many_data: dict[str, dict[str, pd.DataFrame]], client: Async
|
|
17
19
|
return await _c10346.save_many(COL_NAME, many_data, client)
|
18
20
|
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
+
ReturnType = C103 | dict[str, pd.DataFrame] | None
|
23
|
+
|
24
|
+
|
25
|
+
async def get_latest(code: str, client: AsyncIOMotorClient, as_type: Literal["model", "dataframe"] = "model") -> ReturnType:
|
26
|
+
if as_type == "model":
|
27
|
+
return await _c10346.get_latest_as_model(COL_NAME, code, client)
|
28
|
+
|
29
|
+
elif as_type == "dataframe":
|
30
|
+
return await _c10346.get_latest_doc_as_df_dict(COL_NAME, code, client)
|
31
|
+
|
32
|
+
else:
|
33
|
+
raise ValueError(f"지원하지 않는 반환 타입: '{as_type}' (허용값: 'model', 'dataframe')")
|
22
34
|
|
23
35
|
|
24
36
|
async def has_doc_changed(code: str, client: AsyncIOMotorClient) -> bool:
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
1
3
|
from motor.motor_asyncio import AsyncIOMotorClient
|
2
4
|
import pandas as pd
|
3
5
|
|
@@ -17,8 +19,18 @@ async def save_many(many_data: dict[str, dict[str, pd.DataFrame]], client: Async
|
|
17
19
|
return await _c10346.save_many(COL_NAME, many_data, client)
|
18
20
|
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
+
ReturnType = C104 | dict[str, pd.DataFrame] | None
|
23
|
+
|
24
|
+
|
25
|
+
async def get_latest(code: str, client: AsyncIOMotorClient, as_type: Literal["model", "dataframe"] = "model") -> ReturnType:
|
26
|
+
if as_type == "model":
|
27
|
+
return await _c10346.get_latest_as_model(COL_NAME, code, client)
|
28
|
+
|
29
|
+
elif as_type == "dataframe":
|
30
|
+
return await _c10346.get_latest_doc_as_df_dict(COL_NAME, code, client)
|
31
|
+
|
32
|
+
else:
|
33
|
+
raise ValueError(f"지원하지 않는 반환 타입: '{as_type}' (허용값: 'model', 'dataframe')")
|
22
34
|
|
23
35
|
|
24
36
|
async def has_doc_changed(code: str, client: AsyncIOMotorClient) -> bool:
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
1
3
|
from motor.motor_asyncio import AsyncIOMotorClient
|
2
4
|
import pandas as pd
|
3
5
|
|
@@ -17,8 +19,18 @@ async def save_many(many_data: dict[str, dict[str, pd.DataFrame]], client: Async
|
|
17
19
|
return await _c10346.save_many(COL_NAME, many_data, client)
|
18
20
|
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
+
ReturnType = C106 | dict[str, pd.DataFrame] | None
|
23
|
+
|
24
|
+
|
25
|
+
async def get_latest(code: str, client: AsyncIOMotorClient, as_type: Literal["model", "dataframe"] = "model") -> ReturnType:
|
26
|
+
if as_type == "model":
|
27
|
+
return await _c10346.get_latest_as_model(COL_NAME, code, client)
|
28
|
+
|
29
|
+
elif as_type == "dataframe":
|
30
|
+
return await _c10346.get_latest_doc_as_df_dict(COL_NAME, code, client)
|
31
|
+
|
32
|
+
else:
|
33
|
+
raise ValueError(f"지원하지 않는 반환 타입: '{as_type}' (허용값: 'model', 'dataframe')")
|
22
34
|
|
23
35
|
|
24
36
|
async def has_doc_changed(code: str, client: AsyncIOMotorClient) -> bool:
|
@@ -1,10 +1,15 @@
|
|
1
1
|
from pydantic import BaseModel, Field, field_serializer, ConfigDict, field_validator
|
2
2
|
from datetime import datetime
|
3
3
|
|
4
|
+
class CodeName(BaseModel):
|
5
|
+
코드: str
|
6
|
+
종목명: str | None
|
7
|
+
|
4
8
|
class C101(BaseModel):
|
5
9
|
id: str | None = Field(default=None, alias="_id")
|
6
|
-
날짜: datetime
|
7
10
|
코드: str
|
11
|
+
날짜: datetime
|
12
|
+
종목명: str | None
|
8
13
|
bps: int | None
|
9
14
|
eps: int | None
|
10
15
|
pbr: float | None
|
@@ -26,7 +31,7 @@ class C101(BaseModel):
|
|
26
31
|
외국인지분율: float | None
|
27
32
|
유동비율: float | None
|
28
33
|
전일대비: int | None
|
29
|
-
|
34
|
+
|
30
35
|
주가: int | None
|
31
36
|
최고52: int | None
|
32
37
|
최저52: int | None
|
@@ -0,0 +1,9 @@
|
|
1
|
+
DB_NAME = "valuation"
|
2
|
+
DATE_FORMAT = "%Y.%m.%d"
|
3
|
+
|
4
|
+
from db2_hj3415.common import connection
|
5
|
+
from db2_hj3415.common.utils import *
|
6
|
+
from db2_hj3415.common.db_ops import *
|
7
|
+
from db2_hj3415.nfs._ops import get_all_codes_sync, get_all_codes
|
8
|
+
|
9
|
+
from db2_hj3415.valuation.models import *
|
@@ -0,0 +1,65 @@
|
|
1
|
+
from pymongo import ASCENDING
|
2
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
3
|
+
from datetime import datetime, timezone, time
|
4
|
+
|
5
|
+
from db2_hj3415.valuation import DB_NAME, MilData, RedData
|
6
|
+
from db2_hj3415.common.db_ops import get_collection
|
7
|
+
from utils_hj3415 import setup_logger
|
8
|
+
from bson import ObjectId
|
9
|
+
|
10
|
+
mylogger = setup_logger(__name__, 'WARNING')
|
11
|
+
|
12
|
+
T = RedData | MilData
|
13
|
+
|
14
|
+
async def save(col: str, data: T, client: AsyncIOMotorClient) -> dict:
|
15
|
+
collection = get_collection(client, DB_NAME, col)
|
16
|
+
await collection.create_index([("날짜", ASCENDING), ("코드", ASCENDING)], unique=True)
|
17
|
+
|
18
|
+
data.날짜 = datetime.now(timezone.utc)
|
19
|
+
|
20
|
+
# 날짜 기준 중복 확인
|
21
|
+
today = data.날짜.date()
|
22
|
+
|
23
|
+
existing = await collection.find_one({
|
24
|
+
"코드": data.코드,
|
25
|
+
"날짜": {
|
26
|
+
"$gte": datetime.combine(today, time.min).replace(tzinfo=timezone.utc),
|
27
|
+
"$lt": datetime.combine(today, time.max).replace(tzinfo=timezone.utc)
|
28
|
+
}
|
29
|
+
})
|
30
|
+
mylogger.debug(f"이미 저장된 오늘 날짜 데이터가 있나?: {existing}")
|
31
|
+
|
32
|
+
if existing:
|
33
|
+
return {"status": "skipped", "reason": "already_saved_today"}
|
34
|
+
|
35
|
+
# datetime 그대로 유지하기 위해 mode='python' 사용
|
36
|
+
doc = data.model_dump(by_alias=True, mode='python', exclude_none=False)
|
37
|
+
|
38
|
+
# ObjectId가 존재하면 업데이트, 아니면 삽입
|
39
|
+
if '_id' in doc:
|
40
|
+
if doc['_id'] is None:
|
41
|
+
doc.pop('_id') # None이면 제거하여 MongoDB에서 자동 생성되게 함
|
42
|
+
else:
|
43
|
+
doc['_id'] = ObjectId(doc['_id']) if isinstance(doc['_id'], str) else doc['_id']
|
44
|
+
await collection.replace_one({'_id': doc['_id']}, doc, upsert=True)
|
45
|
+
return {"status": "updated", "_id": str(doc['_id'])}
|
46
|
+
|
47
|
+
result = await collection.insert_one(doc)
|
48
|
+
data.id = str(result.inserted_id)
|
49
|
+
return {"status": "inserted", "_id": data.id}
|
50
|
+
|
51
|
+
|
52
|
+
async def save_many(col: str, many_data: dict[str, T], client: AsyncIOMotorClient) -> dict:
|
53
|
+
results = {}
|
54
|
+
|
55
|
+
# 이 방식이 속도는 느리지만 제일 간단하고 안정적인 방식임.
|
56
|
+
for code, data in many_data.items():
|
57
|
+
try:
|
58
|
+
result = await save(col, data, client)
|
59
|
+
results[code] = result
|
60
|
+
except Exception as e:
|
61
|
+
# 에러 발생 시 로깅 또는 실패 처리
|
62
|
+
results[code] = {"status": "error", "error": str(e)}
|
63
|
+
mylogger.error(f"[{code}] 저장 중 오류 발생: {e}")
|
64
|
+
|
65
|
+
return results
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
2
|
+
|
3
|
+
from db2_hj3415.valuation import BlueData, _ops
|
4
|
+
from utils_hj3415 import setup_logger
|
5
|
+
|
6
|
+
|
7
|
+
mylogger = setup_logger(__name__, 'WARNING')
|
8
|
+
|
9
|
+
COL_NAME = "blue"
|
10
|
+
|
11
|
+
|
12
|
+
async def save(blue_data: BlueData, client: AsyncIOMotorClient) -> dict:
|
13
|
+
return await _ops.save(COL_NAME, blue_data, client)
|
14
|
+
|
15
|
+
|
16
|
+
async def save_many(many_data: dict[str, BlueData], client: AsyncIOMotorClient) -> dict:
|
17
|
+
return await _ops.save_many(COL_NAME, many_data, client)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
2
|
+
|
3
|
+
from db2_hj3415.valuation import GrowthData, _ops
|
4
|
+
from utils_hj3415 import setup_logger
|
5
|
+
|
6
|
+
|
7
|
+
mylogger = setup_logger(__name__, 'WARNING')
|
8
|
+
|
9
|
+
COL_NAME = "growth"
|
10
|
+
|
11
|
+
|
12
|
+
async def save(growth_data: GrowthData, client: AsyncIOMotorClient) -> dict:
|
13
|
+
return await _ops.save(COL_NAME, growth_data, client)
|
14
|
+
|
15
|
+
|
16
|
+
async def save_many(many_data: dict[str, GrowthData], client: AsyncIOMotorClient) -> dict:
|
17
|
+
return await _ops.save_many(COL_NAME, many_data, client)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
2
|
+
|
3
|
+
from db2_hj3415.valuation import MilData, _ops
|
4
|
+
from utils_hj3415 import setup_logger
|
5
|
+
|
6
|
+
|
7
|
+
mylogger = setup_logger(__name__, 'WARNING')
|
8
|
+
|
9
|
+
COL_NAME = "mil"
|
10
|
+
|
11
|
+
|
12
|
+
async def save(mil_data: MilData, client: AsyncIOMotorClient) -> dict:
|
13
|
+
return await _ops.save(COL_NAME, mil_data, client)
|
14
|
+
|
15
|
+
|
16
|
+
async def save_many(many_data: dict[str, MilData], client: AsyncIOMotorClient) -> dict:
|
17
|
+
return await _ops.save_many(COL_NAME, many_data, client)
|
@@ -0,0 +1,193 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field, field_validator, field_serializer, ConfigDict, model_validator, SerializationInfo
|
4
|
+
from datetime import datetime
|
5
|
+
import math
|
6
|
+
from utils_hj3415 import tools
|
7
|
+
|
8
|
+
|
9
|
+
class RedData(BaseModel):
|
10
|
+
id: str | None = Field(default=None, alias="_id")
|
11
|
+
코드: str
|
12
|
+
날짜: datetime | None = Field(default=None)
|
13
|
+
종목명: str
|
14
|
+
|
15
|
+
사업가치: float | None
|
16
|
+
지배주주당기순이익: float | None
|
17
|
+
expect_earn: float | None
|
18
|
+
|
19
|
+
재산가치: float | None
|
20
|
+
유동자산: float | None
|
21
|
+
유동부채: float | None
|
22
|
+
투자자산: float | None
|
23
|
+
투자부동산: float | None
|
24
|
+
|
25
|
+
부채평가: float | None
|
26
|
+
발행주식수: int | None
|
27
|
+
|
28
|
+
자료제출일: list[str] | None = Field(default=None)
|
29
|
+
주가: float | None
|
30
|
+
red_price: float | None
|
31
|
+
score: int | None
|
32
|
+
|
33
|
+
@model_validator(mode='before')
|
34
|
+
@classmethod
|
35
|
+
def replace_nan_with_none(cls, values: dict) -> dict:
|
36
|
+
return {
|
37
|
+
k: (None if isinstance(v, float) and math.isnan(v) else v)
|
38
|
+
for k, v in values.items()
|
39
|
+
}
|
40
|
+
|
41
|
+
@field_serializer("날짜")
|
42
|
+
def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
|
43
|
+
# JSON 응답용일 때만 문자열로 직렬화
|
44
|
+
if info.mode == 'json':
|
45
|
+
return value.isoformat()
|
46
|
+
return value
|
47
|
+
|
48
|
+
@field_validator("코드")
|
49
|
+
@classmethod
|
50
|
+
def validate_코드(cls, v):
|
51
|
+
if not tools.is_6digit(v):
|
52
|
+
raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
|
53
|
+
return v
|
54
|
+
|
55
|
+
model_config = ConfigDict(
|
56
|
+
populate_by_name=True,
|
57
|
+
str_strip_whitespace=True,
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
class Evaluation(BaseModel):
|
62
|
+
최근값: float | None = Field(default=None)
|
63
|
+
시계열: dict[str, float] = Field(default_factory=dict)
|
64
|
+
평가결과: dict[str, Any] = Field(default_factory=dict)
|
65
|
+
model_config = ConfigDict(
|
66
|
+
extra="allow"
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
class MilData(BaseModel):
|
71
|
+
id: str | None = Field(default=None, alias="_id")
|
72
|
+
코드: str
|
73
|
+
날짜: datetime | None = Field(default=None)
|
74
|
+
종목명: str
|
75
|
+
|
76
|
+
주주수익률: float | None
|
77
|
+
이익지표: float | None
|
78
|
+
|
79
|
+
# 투자수익률
|
80
|
+
ROIC: Evaluation
|
81
|
+
ROE: Evaluation
|
82
|
+
ROA: Evaluation
|
83
|
+
|
84
|
+
# 가치지표
|
85
|
+
FCF: Evaluation
|
86
|
+
PFCF: Evaluation
|
87
|
+
PCR: Evaluation
|
88
|
+
|
89
|
+
@model_validator(mode='before')
|
90
|
+
@classmethod
|
91
|
+
def replace_nan_with_none(cls, values: dict) -> dict:
|
92
|
+
return {
|
93
|
+
k: (None if isinstance(v, float) and math.isnan(v) else v)
|
94
|
+
for k, v in values.items()
|
95
|
+
}
|
96
|
+
|
97
|
+
@field_serializer("날짜")
|
98
|
+
def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
|
99
|
+
# JSON 응답용일 때만 문자열로 직렬화
|
100
|
+
if info.mode == 'json':
|
101
|
+
return value.isoformat()
|
102
|
+
return value
|
103
|
+
|
104
|
+
@field_validator("코드")
|
105
|
+
@classmethod
|
106
|
+
def validate_코드(cls, v):
|
107
|
+
if not tools.is_6digit(v):
|
108
|
+
raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
|
109
|
+
return v
|
110
|
+
|
111
|
+
model_config = ConfigDict(
|
112
|
+
populate_by_name=True,
|
113
|
+
str_strip_whitespace=True,
|
114
|
+
)
|
115
|
+
|
116
|
+
|
117
|
+
class BlueData(BaseModel):
|
118
|
+
id: str | None = Field(default=None, alias="_id")
|
119
|
+
코드: str
|
120
|
+
날짜: datetime | None = Field(default=None)
|
121
|
+
종목명: str
|
122
|
+
|
123
|
+
유동비율: float
|
124
|
+
|
125
|
+
재고자산회전율: Evaluation
|
126
|
+
이자보상배율: Evaluation
|
127
|
+
순운전자본회전율: Evaluation | None
|
128
|
+
순부채비율: Evaluation
|
129
|
+
|
130
|
+
자료제출일: list[str] | None = Field(default=None)
|
131
|
+
|
132
|
+
@model_validator(mode='before')
|
133
|
+
@classmethod
|
134
|
+
def replace_nan_with_none(cls, values: dict) -> dict:
|
135
|
+
return {
|
136
|
+
k: (None if isinstance(v, float) and math.isnan(v) else v)
|
137
|
+
for k, v in values.items()
|
138
|
+
}
|
139
|
+
|
140
|
+
@field_serializer("날짜")
|
141
|
+
def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
|
142
|
+
# JSON 응답용일 때만 문자열로 직렬화
|
143
|
+
if info.mode == 'json':
|
144
|
+
return value.isoformat()
|
145
|
+
return value
|
146
|
+
|
147
|
+
@field_validator("코드")
|
148
|
+
@classmethod
|
149
|
+
def validate_코드(cls, v):
|
150
|
+
if not tools.is_6digit(v):
|
151
|
+
raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
|
152
|
+
return v
|
153
|
+
|
154
|
+
model_config = ConfigDict(
|
155
|
+
populate_by_name=True,
|
156
|
+
str_strip_whitespace=True,
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
class GrowthData(BaseModel):
|
161
|
+
id: str | None = Field(default=None, alias="_id")
|
162
|
+
코드: str
|
163
|
+
날짜: datetime | None = Field(default=None)
|
164
|
+
종목명: str
|
165
|
+
|
166
|
+
매출액증가율: Evaluation
|
167
|
+
|
168
|
+
@model_validator(mode='before')
|
169
|
+
@classmethod
|
170
|
+
def replace_nan_with_none(cls, values: dict) -> dict:
|
171
|
+
return {
|
172
|
+
k: (None if isinstance(v, float) and math.isnan(v) else v)
|
173
|
+
for k, v in values.items()
|
174
|
+
}
|
175
|
+
|
176
|
+
@field_serializer("날짜")
|
177
|
+
def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
|
178
|
+
# JSON 응답용일 때만 문자열로 직렬화
|
179
|
+
if info.mode == 'json':
|
180
|
+
return value.isoformat()
|
181
|
+
return value
|
182
|
+
|
183
|
+
@field_validator("코드")
|
184
|
+
@classmethod
|
185
|
+
def validate_코드(cls, v):
|
186
|
+
if not tools.is_6digit(v):
|
187
|
+
raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
|
188
|
+
return v
|
189
|
+
|
190
|
+
model_config = ConfigDict(
|
191
|
+
populate_by_name=True,
|
192
|
+
str_strip_whitespace=True,
|
193
|
+
)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
2
|
+
|
3
|
+
from db2_hj3415.valuation import RedData, _ops
|
4
|
+
from utils_hj3415 import setup_logger
|
5
|
+
|
6
|
+
|
7
|
+
mylogger = setup_logger(__name__, 'WARNING')
|
8
|
+
|
9
|
+
COL_NAME = "red"
|
10
|
+
|
11
|
+
|
12
|
+
async def save(red_data: RedData, client: AsyncIOMotorClient) -> dict:
|
13
|
+
return await _ops.save(COL_NAME, red_data, client)
|
14
|
+
|
15
|
+
|
16
|
+
async def save_many(many_data: dict[str, RedData], client: AsyncIOMotorClient) -> dict:
|
17
|
+
return await _ops.save_many(COL_NAME, many_data, client)
|
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "db2_hj3415"
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.9"
|
8
8
|
authors = [{name = "Hyungjin Kim", email = "hj3415@gmail.com"}]
|
9
9
|
description = "Gathering the stock data by playwright"
|
10
10
|
readme = "README.md"
|
@@ -15,7 +15,6 @@ dependencies = [
|
|
15
15
|
"pandas-stubs",
|
16
16
|
"deepdiff",
|
17
17
|
"utils_hj3415>=3.2.3",
|
18
|
-
"redis",
|
19
18
|
"pydantic",
|
20
19
|
]
|
21
20
|
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# MongoDB/Redis 연결
|
2
|
-
import os
|
3
|
-
from motor.motor_asyncio import AsyncIOMotorClient
|
4
|
-
import redis.asyncio as redis
|
5
|
-
from redis.asyncio.client import Redis
|
6
|
-
|
7
|
-
# 싱글톤 몽고 클라이언트 정의
|
8
|
-
MONGO_URI = os.getenv("MONGO_ADDR", "mongodb://localhost:27017")
|
9
|
-
client: AsyncIOMotorClient = None
|
10
|
-
|
11
|
-
def get_mongo_client() -> AsyncIOMotorClient:
|
12
|
-
global client
|
13
|
-
if client is None:
|
14
|
-
client = AsyncIOMotorClient(MONGO_URI)
|
15
|
-
return client
|
16
|
-
|
17
|
-
def close_mongo_client():
|
18
|
-
if client:
|
19
|
-
client.close()
|
20
|
-
|
21
|
-
|
22
|
-
async def get_redis() -> Redis:
|
23
|
-
client = redis.Redis(host="localhost", port=6379, decode_responses=True)
|
24
|
-
try:
|
25
|
-
await client.ping()
|
26
|
-
return client
|
27
|
-
except Exception as e:
|
28
|
-
print("Redis connection failed:", e)
|
29
|
-
return None
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# 자주 쓰는 간단한 유틸 함수
|
2
|
-
import pandas as pd
|
3
|
-
import numpy as np
|
4
|
-
|
5
|
-
def df_to_dict_replace_nan(df: pd.DataFrame) -> list[dict]:
|
6
|
-
# NaN → None으로 변환
|
7
|
-
return df.replace({np.nan: None}).to_dict(orient="records")
|
8
|
-
|
9
|
-
import json
|
10
|
-
from bson import json_util
|
11
|
-
from pydantic import BaseModel
|
12
|
-
|
13
|
-
def pretty_print(obj):
|
14
|
-
if isinstance(obj, BaseModel):
|
15
|
-
# Pydantic 모델이면 dict로 변환
|
16
|
-
data = obj.model_dump(by_alias=True)
|
17
|
-
elif isinstance(obj, list) and all(isinstance(o, BaseModel) for o in obj):
|
18
|
-
# 리스트 안에 BaseModel만 있다면 변환
|
19
|
-
data = [o.model_dump(by_alias=True) for o in obj]
|
20
|
-
else:
|
21
|
-
# 일반 dict나 기타 객체
|
22
|
-
data = obj
|
23
|
-
|
24
|
-
print(json.dumps(data, indent=2, ensure_ascii=False, default=json_util.default))
|
@@ -1,42 +0,0 @@
|
|
1
|
-
from motor.motor_asyncio import AsyncIOMotorClient
|
2
|
-
from db2_hj3415.nfs import DB_NAME
|
3
|
-
|
4
|
-
|
5
|
-
async def get_all_codes(client: AsyncIOMotorClient) -> list[str]:
|
6
|
-
"""
|
7
|
-
c103, c104, c106 컬렉션에 모두 존재하는 코드의 리스트를 반환함.
|
8
|
-
|
9
|
-
Args:
|
10
|
-
client (AsyncIOMotorClient): MongoDB 비동기 클라이언트 객체
|
11
|
-
|
12
|
-
Returns:
|
13
|
-
list[str]: c103, c104, c106 컬렉션에 공통으로 존재하는 종목 코드 리스트
|
14
|
-
"""
|
15
|
-
db = client[DB_NAME]
|
16
|
-
|
17
|
-
collections = ['c103', 'c104', 'c106']
|
18
|
-
|
19
|
-
# 첫 컬렉션으로 초기화
|
20
|
-
s = set(await db[collections[0]].distinct("코드"))
|
21
|
-
|
22
|
-
for col in collections[1:]:
|
23
|
-
codes = await db[col].distinct("코드")
|
24
|
-
s &= set(codes)
|
25
|
-
|
26
|
-
return list(s)
|
27
|
-
|
28
|
-
|
29
|
-
async def delete_code_from_all_collections(code: str, client: AsyncIOMotorClient) -> dict[str, int]:
|
30
|
-
db = client[DB_NAME]
|
31
|
-
|
32
|
-
collections = ['c101', 'c103', 'c104', 'c106', 'c108']
|
33
|
-
|
34
|
-
deleted_counts = {}
|
35
|
-
|
36
|
-
for col in collections:
|
37
|
-
result = await db[col].delete_many({"코드": code})
|
38
|
-
deleted_counts[col] = result.deleted_count
|
39
|
-
|
40
|
-
print(f"삭제된 도큐먼트 갯수: {deleted_counts}")
|
41
|
-
return deleted_counts
|
42
|
-
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|