krx-hj3415 2.2.3__tar.gz → 2.3.0__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.
- {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/PKG-INFO +2 -2
- {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/pyproject.toml +2 -2
- krx_hj3415-2.3.0/src/krx_hj3415/app/adapters/nfs_store_db2.py +16 -0
- krx_hj3415-2.3.0/src/krx_hj3415/app/adapters/universe_store_db2.py +30 -0
- {krx_hj3415-2.2.3/src/krx_hj3415 → krx_hj3415-2.3.0/src/krx_hj3415/app}/domain/diff.py +3 -3
- {krx_hj3415-2.2.3/src/krx_hj3415 → krx_hj3415-2.3.0/src/krx_hj3415/app}/domain/types.py +9 -3
- krx_hj3415-2.3.0/src/krx_hj3415/app/ports/__init__.py +0 -0
- krx_hj3415-2.3.0/src/krx_hj3415/app/ports/nfs_cleaner.py +8 -0
- krx_hj3415-2.3.0/src/krx_hj3415/app/ports/universe_store.py +17 -0
- krx_hj3415-2.3.0/src/krx_hj3415/app/provider/__init__.py +0 -0
- {krx_hj3415-2.2.3/src/krx_hj3415 → krx_hj3415-2.3.0/src/krx_hj3415/app}/provider/krx300_samsungfund_excel.py +5 -5
- krx_hj3415-2.3.0/src/krx_hj3415/app/usecases/__init__.py +0 -0
- krx_hj3415-2.3.0/src/krx_hj3415/app/usecases/sync_universe.py +118 -0
- {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/src/krx_hj3415/cli.py +32 -10
- krx_hj3415-2.2.3/src/krx_hj3415/domain/universe.py +0 -8
- krx_hj3415-2.2.3/src/krx_hj3415/usecases/sync_universe.py +0 -136
- {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/LICENSE +0 -0
- {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/README.md +0 -0
- {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/src/krx_hj3415/__init__.py +0 -0
- {krx_hj3415-2.2.3/src/krx_hj3415/domain → krx_hj3415-2.3.0/src/krx_hj3415/app}/__init__.py +0 -0
- {krx_hj3415-2.2.3/src/krx_hj3415/provider → krx_hj3415-2.3.0/src/krx_hj3415/app/adapters}/__init__.py +0 -0
- {krx_hj3415-2.2.3/src/krx_hj3415/usecases → krx_hj3415-2.3.0/src/krx_hj3415/app/domain}/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: krx-hj3415
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: KRX300 code scraper
|
|
5
5
|
Keywords: example,demo
|
|
6
6
|
Author-email: Hyungjin Kim <hj3415@gmail.com>
|
|
@@ -17,7 +17,7 @@ Requires-Dist: xlrd>=2.0.2
|
|
|
17
17
|
Requires-Dist: typer>=0.21.1
|
|
18
18
|
Requires-Dist: db2-hj3415
|
|
19
19
|
Requires-Dist: contracts-hj3415
|
|
20
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: common-hj3415
|
|
21
21
|
|
|
22
22
|
# krx-hj3415
|
|
23
23
|
|
|
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "krx-hj3415" # PyPI 이름 (하이픈 허용)
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "KRX300 code scraper"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
"typer>=0.21.1",
|
|
26
26
|
"db2-hj3415",
|
|
27
27
|
"contracts-hj3415",
|
|
28
|
-
"
|
|
28
|
+
"common-hj3415",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[tool.flit.module]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from pymongo.asynchronous.database import AsyncDatabase
|
|
6
|
+
|
|
7
|
+
from db2_hj3415.nfs.repo import delete_codes_from_nfs
|
|
8
|
+
from krx_hj3415.app.ports.nfs_cleaner import NfsCleanerPort
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NfsCleanerDb2(NfsCleanerPort):
|
|
12
|
+
def __init__(self, db: AsyncDatabase):
|
|
13
|
+
self._db = db
|
|
14
|
+
|
|
15
|
+
async def delete_codes(self, *, codes: Sequence[str]) -> dict[str, int]:
|
|
16
|
+
return await delete_codes_from_nfs(self._db, codes=codes, endpoint=None)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pymongo.asynchronous.database import AsyncDatabase
|
|
4
|
+
|
|
5
|
+
from contracts_hj3415.universe.dto import UniverseDTO
|
|
6
|
+
from contracts_hj3415.universe.types import Universe
|
|
7
|
+
|
|
8
|
+
from db2_hj3415.universe import repo as urepo
|
|
9
|
+
from db2_hj3415.universe import mappers as umap
|
|
10
|
+
|
|
11
|
+
from krx_hj3415.app.ports.universe_store import UniverseStorePort
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UniverseStoreDb2(UniverseStorePort):
|
|
15
|
+
def __init__(self, db: AsyncDatabase):
|
|
16
|
+
self._db = db
|
|
17
|
+
|
|
18
|
+
async def get_latest(self, *, universe: Universe) -> UniverseDTO | None:
|
|
19
|
+
doc = await urepo.get_latest(self._db, universe=universe)
|
|
20
|
+
if not doc:
|
|
21
|
+
return None
|
|
22
|
+
return umap.latest_doc_to_dto(doc)
|
|
23
|
+
|
|
24
|
+
async def upsert_latest(self, *, dto: UniverseDTO) -> None:
|
|
25
|
+
doc = umap.dto_to_latest_doc(dto)
|
|
26
|
+
await urepo.upsert_latest_doc(self._db, doc=doc)
|
|
27
|
+
|
|
28
|
+
async def insert_snapshot(self, *, dto: UniverseDTO) -> None:
|
|
29
|
+
doc = umap.dto_to_snapshot_doc(dto)
|
|
30
|
+
await urepo.insert_snapshot_doc(self._db, doc=doc)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# krx_hj3415/domain/diff.py
|
|
1
|
+
# krx_hj3415/app/domain/diff.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from typing import Iterable
|
|
6
|
-
from .types import UniverseDiff, CodeItem
|
|
6
|
+
from .types import UniverseDiff, CodeItem, UniverseEnum
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _to_code_map(items: Iterable[CodeItem]) -> dict[str, CodeItem]:
|
|
@@ -12,7 +12,7 @@ def _to_code_map(items: Iterable[CodeItem]) -> dict[str, CodeItem]:
|
|
|
12
12
|
|
|
13
13
|
def diff_universe(
|
|
14
14
|
*,
|
|
15
|
-
universe:
|
|
15
|
+
universe: UniverseEnum,
|
|
16
16
|
asof: datetime,
|
|
17
17
|
new_items: list[CodeItem],
|
|
18
18
|
old_items: list[CodeItem],
|
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
# krx_hj3415/domain/types.py
|
|
1
|
+
# krx_hj3415/app/domain/types.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from datetime import datetime
|
|
6
|
+
from enum import StrEnum
|
|
6
7
|
|
|
8
|
+
class UniverseEnum(StrEnum):
|
|
9
|
+
KRX300 = "KRX300"
|
|
10
|
+
|
|
11
|
+
class MarketEnum(StrEnum):
|
|
12
|
+
KRX = "KRX"
|
|
7
13
|
|
|
8
14
|
@dataclass(frozen=True)
|
|
9
15
|
class CodeItem:
|
|
10
16
|
code: str
|
|
11
17
|
name: str
|
|
12
18
|
asof: datetime
|
|
13
|
-
market:
|
|
19
|
+
market: MarketEnum
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
@dataclass(frozen=True)
|
|
17
23
|
class UniverseDiff:
|
|
18
|
-
universe:
|
|
24
|
+
universe: UniverseEnum
|
|
19
25
|
asof: datetime
|
|
20
26
|
added: list[CodeItem]
|
|
21
27
|
removed: list[CodeItem]
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from contracts_hj3415.universe.dto import UniverseDTO
|
|
6
|
+
from contracts_hj3415.universe.types import Universe
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UniverseStorePort(Protocol):
|
|
10
|
+
async def get_latest(self, *, universe: Universe) -> UniverseDTO | None:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
async def upsert_latest(self, *, dto: UniverseDTO) -> None:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
async def insert_snapshot(self, *, dto: UniverseDTO) -> None:
|
|
17
|
+
...
|
|
File without changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# krx_hj3415/provider/krx300_samsungfund_excel.py
|
|
1
|
+
# krx_hj3415/app/provider/krx300_samsungfund_excel.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import random
|
|
@@ -8,9 +8,8 @@ import requests
|
|
|
8
8
|
from datetime import datetime, timedelta, timezone
|
|
9
9
|
from io import BytesIO
|
|
10
10
|
|
|
11
|
-
from
|
|
12
|
-
from krx_hj3415.domain.types import CodeItem
|
|
13
|
-
|
|
11
|
+
from common_hj3415.utils.time import utcnow
|
|
12
|
+
from krx_hj3415.app.domain.types import CodeItem, MarketEnum
|
|
14
13
|
|
|
15
14
|
FUND_ID = "2ETFA4"
|
|
16
15
|
BASE_URL = "https://www.samsungfund.com/excel_pdf.do"
|
|
@@ -55,6 +54,7 @@ def download_excel_bytes(*, max_days: int = 15) -> tuple[bytes, datetime, str]:
|
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def parse_krx300_items(excel_bytes: bytes, *, asof: datetime) -> list[CodeItem]:
|
|
57
|
+
market = MarketEnum.KRX
|
|
58
58
|
# 네가 쓰던 조건: usecols B:I, skiprows 2
|
|
59
59
|
df = pd.read_excel(BytesIO(excel_bytes), usecols="B:I", skiprows=2) # type: ignore
|
|
60
60
|
|
|
@@ -71,7 +71,7 @@ def parse_krx300_items(excel_bytes: bytes, *, asof: datetime) -> list[CodeItem]:
|
|
|
71
71
|
|
|
72
72
|
items: list[CodeItem] = []
|
|
73
73
|
for code, name in zip(df["종목코드"].astype(str), df["종목명"].astype(str)):
|
|
74
|
-
items.append(CodeItem(code=code, name=name, asof=asof))
|
|
74
|
+
items.append(CodeItem(code=code, name=name, asof=asof, market=market))
|
|
75
75
|
return items
|
|
76
76
|
|
|
77
77
|
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# krx_hj3415/app/usecases/sync_universe.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Iterable, cast
|
|
6
|
+
|
|
7
|
+
from common_hj3415.utils.time import utcnow
|
|
8
|
+
|
|
9
|
+
from contracts_hj3415.universe.dto import UniverseItemDTO, UniverseDTO
|
|
10
|
+
from contracts_hj3415.universe.types import Universe
|
|
11
|
+
|
|
12
|
+
from krx_hj3415.app.domain.types import CodeItem, UniverseDiff, UniverseEnum, MarketEnum
|
|
13
|
+
from krx_hj3415.app.domain.diff import diff_universe
|
|
14
|
+
from krx_hj3415.app.provider.krx300_samsungfund_excel import fetch_krx300_items
|
|
15
|
+
|
|
16
|
+
from krx_hj3415.app.ports.universe_store import UniverseStorePort
|
|
17
|
+
from krx_hj3415.app.ports.nfs_cleaner import NfsCleanerPort
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def refresh_krx300(*, max_days: int = 15) -> tuple[datetime, list[CodeItem]]:
|
|
21
|
+
return fetch_krx300_items(max_days=max_days)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _dto_to_code_items(dto: UniverseDTO | None) -> list[CodeItem]:
|
|
25
|
+
if not dto:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
out: list[CodeItem] = []
|
|
29
|
+
for row in dto.get("items", []):
|
|
30
|
+
if not isinstance(row, dict):
|
|
31
|
+
continue
|
|
32
|
+
code = str(row.get("code") or "").strip()
|
|
33
|
+
if not code:
|
|
34
|
+
continue
|
|
35
|
+
name = str(row.get("name") or "").strip()
|
|
36
|
+
market = MarketEnum(row.get("market"))
|
|
37
|
+
# old_item의 asof는 diff 용도라면 dto["asof"]를 쓰는 게 더 자연스럽긴 함(선택)
|
|
38
|
+
out.append(CodeItem(code=code, name=name, asof=utcnow(), market=market))
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _code_items_to_dtos(
|
|
43
|
+
items: Iterable[CodeItem],
|
|
44
|
+
*,
|
|
45
|
+
market: MarketEnum = MarketEnum.KRX,
|
|
46
|
+
) -> list[UniverseItemDTO]:
|
|
47
|
+
"""
|
|
48
|
+
krx 도메인 CodeItem 목록을 contracts UniverseItemDTO 목록으로 변환한다.
|
|
49
|
+
|
|
50
|
+
- CodeItem.code 는 필수
|
|
51
|
+
- CodeItem.name 은 비어있지 않을 때만 포함
|
|
52
|
+
- market 은 krx 정책(default: "KRX")에 따라 주입
|
|
53
|
+
"""
|
|
54
|
+
out: list[UniverseItemDTO] = []
|
|
55
|
+
|
|
56
|
+
for it in items:
|
|
57
|
+
if it is None:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
code = (it.code or "").strip()
|
|
61
|
+
if not code:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
dto: UniverseItemDTO = {
|
|
65
|
+
"code": code,
|
|
66
|
+
"market": market.value,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if isinstance(it.name, str) and it.name.strip():
|
|
70
|
+
dto["name"] = it.name.strip()
|
|
71
|
+
|
|
72
|
+
out.append(dto)
|
|
73
|
+
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def apply_removed(
|
|
78
|
+
cleaner: NfsCleanerPort,
|
|
79
|
+
*,
|
|
80
|
+
removed_codes: Iterable[str],
|
|
81
|
+
) -> dict[str, int]:
|
|
82
|
+
codes = [str(c).strip() for c in removed_codes if c and str(c).strip()]
|
|
83
|
+
if not codes:
|
|
84
|
+
return {"latest_deleted": 0, "snapshots_deleted": 0}
|
|
85
|
+
return await cleaner.delete_codes(codes=codes)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def run_sync(
|
|
89
|
+
universe_store: UniverseStorePort,
|
|
90
|
+
*,
|
|
91
|
+
universe: UniverseEnum = UniverseEnum.KRX300,
|
|
92
|
+
max_days: int = 15,
|
|
93
|
+
snapshot: bool = True,
|
|
94
|
+
) -> UniverseDiff:
|
|
95
|
+
# 1) fetch
|
|
96
|
+
asof, new_items = await refresh_krx300(max_days=max_days)
|
|
97
|
+
|
|
98
|
+
# 2) load old (DTO)
|
|
99
|
+
old_dto = await universe_store.get_latest(universe=cast(Universe, universe.value))
|
|
100
|
+
old_items = _dto_to_code_items(old_dto)
|
|
101
|
+
|
|
102
|
+
# 3) diff
|
|
103
|
+
d = diff_universe(
|
|
104
|
+
universe=universe, asof=asof, new_items=new_items, old_items=old_items
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 4) save (DTO로 저장 요청)
|
|
108
|
+
dto: UniverseDTO = {
|
|
109
|
+
"universe": universe.value,
|
|
110
|
+
"asof": asof,
|
|
111
|
+
"source": "samsungfund",
|
|
112
|
+
"items": _code_items_to_dtos(new_items, market=MarketEnum.KRX),
|
|
113
|
+
}
|
|
114
|
+
await universe_store.upsert_latest(dto=dto)
|
|
115
|
+
if snapshot:
|
|
116
|
+
await universe_store.insert_snapshot(dto=dto)
|
|
117
|
+
|
|
118
|
+
return d
|
|
@@ -9,8 +9,14 @@ from db2_hj3415.settings import get_settings
|
|
|
9
9
|
from db2_hj3415.universe.repo import ensure_indexes as ensure_indexes_universe
|
|
10
10
|
from db2_hj3415.nfs.repo import ensure_indexes as ensure_indexes_nfs
|
|
11
11
|
|
|
12
|
-
from krx_hj3415.
|
|
13
|
-
from krx_hj3415.
|
|
12
|
+
from krx_hj3415.app.ports.nfs_cleaner import NfsCleanerPort
|
|
13
|
+
from krx_hj3415.app.ports.universe_store import UniverseStorePort
|
|
14
|
+
|
|
15
|
+
from krx_hj3415.app.adapters.universe_store_db2 import UniverseStoreDb2
|
|
16
|
+
from krx_hj3415.app.adapters.nfs_store_db2 import NfsCleanerDb2
|
|
17
|
+
|
|
18
|
+
from krx_hj3415.app.domain.types import UniverseEnum
|
|
19
|
+
from krx_hj3415.app.usecases.sync_universe import run_sync, apply_removed
|
|
14
20
|
|
|
15
21
|
app = typer.Typer(no_args_is_help=True)
|
|
16
22
|
|
|
@@ -41,10 +47,16 @@ async def _mongo_bootstrap(db) -> None:
|
|
|
41
47
|
|
|
42
48
|
@app.command()
|
|
43
49
|
def sync(
|
|
44
|
-
universe: str = typer.Argument("
|
|
45
|
-
apply: bool = typer.Option(
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
universe: str = typer.Argument("KRX300", help="유니버스 이름 (현재: KRX300)"),
|
|
51
|
+
apply: bool = typer.Option(
|
|
52
|
+
False, "--apply", help="removed를 nfs에서 실제 삭제까지 적용"
|
|
53
|
+
),
|
|
54
|
+
snapshot: bool = typer.Option(
|
|
55
|
+
True, "--snapshot/--no-snapshot", help="universe snapshot 저장 여부"
|
|
56
|
+
),
|
|
57
|
+
max_days: int = typer.Option(
|
|
58
|
+
15, "--max-days", help="최대 며칠 전까지 유효 엑셀 URL 탐색"
|
|
59
|
+
),
|
|
48
60
|
):
|
|
49
61
|
"""
|
|
50
62
|
1) 외부에서 유니버스 수집
|
|
@@ -59,11 +71,21 @@ def sync(
|
|
|
59
71
|
try:
|
|
60
72
|
await _mongo_bootstrap(db)
|
|
61
73
|
|
|
62
|
-
|
|
74
|
+
store: UniverseStorePort = UniverseStoreDb2(db)
|
|
75
|
+
cleaner: NfsCleanerPort = NfsCleanerDb2(db)
|
|
76
|
+
|
|
77
|
+
d = await run_sync(
|
|
78
|
+
store,
|
|
79
|
+
universe=UniverseEnum(universe),
|
|
80
|
+
max_days=max_days,
|
|
81
|
+
snapshot=snapshot,
|
|
82
|
+
)
|
|
63
83
|
|
|
64
84
|
typer.echo(f"\n=== UNIVERSE SYNC: {d.universe} ===")
|
|
65
85
|
typer.echo(f"asof: {d.asof.isoformat()}")
|
|
66
|
-
typer.echo(
|
|
86
|
+
typer.echo(
|
|
87
|
+
f"added: {len(d.added)}, removed: {len(d.removed)}, kept: {d.kept_count}"
|
|
88
|
+
)
|
|
67
89
|
|
|
68
90
|
if d.added:
|
|
69
91
|
typer.echo("\n[ADDED]")
|
|
@@ -81,7 +103,7 @@ def sync(
|
|
|
81
103
|
|
|
82
104
|
if apply and d.removed:
|
|
83
105
|
typer.echo("\n=== APPLY REMOVED TO NFS ===")
|
|
84
|
-
r = await apply_removed(
|
|
106
|
+
r = await apply_removed(cleaner, removed_codes=d.removed_codes)
|
|
85
107
|
typer.echo(
|
|
86
108
|
f"latest_deleted={r.get('latest_deleted', 0)}, "
|
|
87
109
|
f"snapshots_deleted={r.get('snapshots_deleted', 0)}"
|
|
@@ -96,4 +118,4 @@ def sync(
|
|
|
96
118
|
|
|
97
119
|
|
|
98
120
|
if __name__ == "__main__":
|
|
99
|
-
app()
|
|
121
|
+
app()
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# krx_hj3415/usecases/sync_universe.py
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from typing import Any, Iterable, cast
|
|
6
|
-
from pymongo.asynchronous.database import AsyncDatabase
|
|
7
|
-
|
|
8
|
-
from domain_hj3415.common.time import utcnow
|
|
9
|
-
|
|
10
|
-
from db2_hj3415.nfs.repo import delete_codes_from_nfs
|
|
11
|
-
from db2_hj3415.universe.repo import (
|
|
12
|
-
upsert_latest as upsert_universe_latest,
|
|
13
|
-
insert_snapshot as insert_universe_snapshot,
|
|
14
|
-
)
|
|
15
|
-
from db2_hj3415.universe.repo import get_latest as get_universe_latest
|
|
16
|
-
|
|
17
|
-
from contracts_hj3415.universe.dto import UniverseItemDTO, UniversePayloadDTO
|
|
18
|
-
from contracts_hj3415.universe.types import UniverseNames
|
|
19
|
-
|
|
20
|
-
from krx_hj3415.domain.types import CodeItem, UniverseDiff
|
|
21
|
-
from krx_hj3415.domain.diff import diff_universe
|
|
22
|
-
from krx_hj3415.domain.universe import UniverseKind
|
|
23
|
-
from krx_hj3415.provider.krx300_samsungfund_excel import fetch_krx300_items
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _payload_dto_to_items(dto: UniversePayloadDTO) -> list[CodeItem]:
|
|
27
|
-
if dto is None:
|
|
28
|
-
return []
|
|
29
|
-
|
|
30
|
-
data = dto["items"]
|
|
31
|
-
|
|
32
|
-
out: list[CodeItem] = []
|
|
33
|
-
for row in data:
|
|
34
|
-
if not isinstance(row, dict):
|
|
35
|
-
continue
|
|
36
|
-
code = str(row.get("code") or "").strip()
|
|
37
|
-
if not code:
|
|
38
|
-
continue
|
|
39
|
-
name = str(row.get("name") or "").strip()
|
|
40
|
-
market = row.get("market")
|
|
41
|
-
asof = utcnow()
|
|
42
|
-
out.append(CodeItem(code=code, name=name, asof=asof, market=market))
|
|
43
|
-
return out
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
async def refresh_krx300(*, max_days: int = 15) -> tuple[datetime, list[CodeItem]]:
|
|
47
|
-
# 현재는 KRX300만 구현. 다른 universe도 늘리면 여기서 분기
|
|
48
|
-
return fetch_krx300_items(max_days=max_days)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def to_universe_item_dtos(
|
|
52
|
-
items: Iterable[Any], *, market: str = "KRX"
|
|
53
|
-
) -> list[UniverseItemDTO]:
|
|
54
|
-
out: list[UniverseItemDTO] = []
|
|
55
|
-
for it in items:
|
|
56
|
-
if it is None:
|
|
57
|
-
continue
|
|
58
|
-
|
|
59
|
-
if isinstance(it, dict):
|
|
60
|
-
code = (it.get("code") or "").strip()
|
|
61
|
-
name = it.get("name")
|
|
62
|
-
else:
|
|
63
|
-
code = (getattr(it, "code", "") or "").strip()
|
|
64
|
-
name = getattr(it, "name", None)
|
|
65
|
-
|
|
66
|
-
if not code:
|
|
67
|
-
continue
|
|
68
|
-
|
|
69
|
-
dto: UniverseItemDTO = {"code": code, "market": market}
|
|
70
|
-
if isinstance(name, str) and name.strip():
|
|
71
|
-
dto["name"] = name.strip()
|
|
72
|
-
|
|
73
|
-
out.append(dto)
|
|
74
|
-
return out
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
async def run_sync(
|
|
78
|
-
db: AsyncDatabase,
|
|
79
|
-
*,
|
|
80
|
-
universe: UniverseKind = UniverseKind.KRX300,
|
|
81
|
-
max_days: int = 15,
|
|
82
|
-
snapshot: bool = True,
|
|
83
|
-
) -> UniverseDiff:
|
|
84
|
-
"""
|
|
85
|
-
1) 외부에서 최신 유니버스 수집
|
|
86
|
-
2) DB에서 이전 latest 조회
|
|
87
|
-
3) diff 계산
|
|
88
|
-
4) latest upsert + (선택) snapshots insert
|
|
89
|
-
"""
|
|
90
|
-
# --- 1) fetch ---
|
|
91
|
-
asof, new_items = await refresh_krx300(max_days=max_days)
|
|
92
|
-
|
|
93
|
-
# --- 2) load old ---
|
|
94
|
-
old_payload_dto = await get_universe_latest(
|
|
95
|
-
db, universe=cast(UniverseNames, universe.value)
|
|
96
|
-
)
|
|
97
|
-
old_items = _payload_dto_to_items(old_payload_dto)
|
|
98
|
-
|
|
99
|
-
# --- 3) diff ---
|
|
100
|
-
d = diff_universe(
|
|
101
|
-
universe=universe.value, asof=asof, new_items=new_items, old_items=old_items
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
# --- 4) save ---
|
|
105
|
-
await upsert_universe_latest(
|
|
106
|
-
db,
|
|
107
|
-
universe=cast(UniverseNames, universe.value),
|
|
108
|
-
items=to_universe_item_dtos(new_items, market="KRX"),
|
|
109
|
-
asof=asof,
|
|
110
|
-
source="samsungfund",
|
|
111
|
-
)
|
|
112
|
-
if snapshot:
|
|
113
|
-
await insert_universe_snapshot(
|
|
114
|
-
db,
|
|
115
|
-
universe=cast(UniverseNames, universe.value),
|
|
116
|
-
items=to_universe_item_dtos(new_items, market="KRX"),
|
|
117
|
-
asof=asof,
|
|
118
|
-
source="samsungfund",
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
return d
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
async def apply_removed(
|
|
125
|
-
db: AsyncDatabase,
|
|
126
|
-
*,
|
|
127
|
-
removed_codes: Iterable[str],
|
|
128
|
-
) -> dict[str, int]:
|
|
129
|
-
"""
|
|
130
|
-
removed codes를 nfs(latest/snapshots)에서 모두 삭제.
|
|
131
|
-
"""
|
|
132
|
-
codes = [str(c).strip() for c in removed_codes if c and str(c).strip()]
|
|
133
|
-
if not codes:
|
|
134
|
-
return {"latest_deleted": 0, "snapshots_deleted": 0}
|
|
135
|
-
|
|
136
|
-
return await delete_codes_from_nfs(db, codes=codes, endpoint=None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{krx_hj3415-2.2.3/src/krx_hj3415/usecases → krx_hj3415-2.3.0/src/krx_hj3415/app/domain}/__init__.py
RENAMED
|
File without changes
|