Files
hyowons 69ef9c09e8 auto: 일일 백업 2026-06-09 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 02:00:01 +09:00

116 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""기준지수(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