auto: 일일 백업 2026-06-09 02:00

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-09 02:00:01 +09:00
parent 7f3994d98d
commit 69ef9c09e8
20 changed files with 3170 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
"""기준지수(KOSPI/KOSDAQ) 일별 종가 캐시 + 벤치마크 수익률·알파.
네이버 m.stock 지수 일별시세 API에서 받아 state/sim/index_history.json 에 누적.
sim/백테스트 수익률을 '같은 기간 지수 매수후보유' 대비(알파)로 평가하기 위한 데이터원.
실패해도 raise 하지 않고 None 으로 흘려보낸다 (지표 부재 ≠ 엔진 중단).
"""
from __future__ import annotations
import json
import urllib.request
from . import config
INDEX_PATH = config.STATE_DIR / 'index_history.json'
INDICES = ('KOSPI', 'KOSDAQ')
INDEX_LABEL = {'KOSPI': '코스피', 'KOSDAQ': '코스닥'}
def norm_date(d: str) -> str:
"""'2026-06-09', '2026-06-09T...', '20260609''YYYYMMDD'."""
return (d or '').replace('-', '')[:8]
def _load() -> dict:
try:
return json.loads(INDEX_PATH.read_text())
except Exception:
return {}
def _save(data: dict) -> None:
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
INDEX_PATH.write_text(json.dumps(data, ensure_ascii=False))
def _fetch_page(index: str, page: int, size: int) -> list[dict]:
url = f'https://m.stock.naver.com/api/index/{index}/price?pageSize={size}&page={page}'
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=6.0) as r:
return json.loads(r.read().decode('utf-8', 'ignore')) or []
def backfill(pages: int = 8, size: int = 50) -> dict:
"""KOSPI/KOSDAQ 일별 종가를 pages×size 만큼 받아 캐시에 병합(idempotent). {index: 신규건수}."""
data = _load()
added: dict[str, int] = {}
for idx in INDICES:
store = data.setdefault(idx, {})
n0 = len(store)
for p in range(1, pages + 1):
try:
rows = _fetch_page(idx, p, size)
except Exception:
break
if not rows:
break
for row in rows:
d = norm_date(row.get('localTradedAt'))
c = row.get('closePrice')
if not d or not c:
continue
try:
store[d] = float(str(c).replace(',', ''))
except (ValueError, TypeError):
pass
added[idx] = len(store) - n0
_save(data)
return added
def update_today() -> None:
"""최신 1페이지만 받아 오늘 종가 갱신 (스캔마다 저비용 호출, 실패 무시)."""
try:
backfill(pages=1, size=10)
except Exception:
pass
def series(index: str) -> dict:
return _load().get(index, {})
def _nearest(store: dict, date: str, after: bool):
"""date 기준 on-or-after(after=True) / on-or-before 가장 가까운 (date, close). 없으면 None."""
if not store:
return None
d = norm_date(date)
keys = sorted(store)
if after:
cand = [k for k in keys if k >= d]
k = cand[0] if cand else None
else:
cand = [k for k in keys if k <= d]
k = cand[-1] if cand else None
return (k, store[k]) if k else None
def benchmark_return(index: str, date_from: str, date_to: str):
"""[from, to] 구간 지수 등락률(%). from=on-or-after, to=on-or-before 종가. 데이터 없으면 None."""
store = series(index)
a = _nearest(store, date_from, after=True)
b = _nearest(store, date_to, after=False)
if not a or not b or a[1] <= 0 or b[0] <= a[0]:
return None
return round((b[1] / a[1] - 1) * 100, 2)
def compare(return_pct, date_from: str, date_to: str) -> dict:
"""기간 수익률(return_pct)을 지수 대비로 비교. {index: {'pct':지수등락, 'alpha':초과}}."""
out: dict[str, dict] = {}
for idx in INDICES:
b = benchmark_return(idx, date_from, date_to)
alpha = round(return_pct - b, 2) if (b is not None and return_pct is not None) else None
out[idx] = {'pct': b, 'alpha': alpha}
return out