feat(kis-collection): finalize sqlite migration, add fallback resilience, and update WBS documentation

This commit is contained in:
2026-06-22 18:34:56 +09:00
parent c576138829
commit 6c549b7bdc
48 changed files with 34610 additions and 24883 deletions
+133 -7
View File
@@ -1,7 +1,7 @@
"""qualitative_sell_strategy_v1 입력 ctx 조립 오케스트레이터.
데이터 출처 (2026-06-21 세션 실측 기준, KIS Open API 연동 이후):
- relative_return_20d, volume_ratio_5d ← tools/fetch_naver_market_data_v1.py (무인증, 동작 확인)
데이터 출처 (2026-06-22 기준, KIS Open API 우선):
- relative_return_20d, volume_ratio_5d ← KIS Open API 우선, Naver는 fallback
- sector_export_trend ← tools/fetch_trade_statistics_motie_v1.py (--csv 경로 권장)
- short_turnover_share ← [신규] KIS Open API daily-short-sale(FHPST04830000)
output2.ssts_vol_rlim — 실측 동작 확인(실전계좌 도메인,
@@ -81,6 +81,125 @@ def _parse_date(value: str | None) -> dt.date | None:
return None
def _coerce_price_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
normalized: list[dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
normalized.append(
{
"date": str(row.get("date") or "").strip(),
"close": row.get("close"),
"open": row.get("open"),
"high": row.get("high"),
"low": row.get("low"),
"volume": row.get("volume"),
}
)
return [row for row in normalized if row["date"]]
def _parse_kis_price_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for key in ("output2", "output1", "output"):
items = payload.get(key)
if not isinstance(items, list):
continue
for item in items:
if not isinstance(item, dict):
continue
date = str(
item.get("stck_bsop_date")
or item.get("data_date")
or item.get("trd_dd")
or item.get("date")
or ""
).strip()
close = item.get("stck_clpr") or item.get("close") or item.get("price")
volume = item.get("acml_vol") or item.get("volume") or item.get("trd_vol") or 0
if not date:
continue
try:
close_val = float(str(close).replace(",", ""))
except Exception:
close_val = 0.0
try:
volume_val = float(str(volume).replace(",", ""))
except Exception:
volume_val = 0.0
rows.append(
{
"date": date.replace(".", "-"),
"close": close_val,
"open": float(str(item.get("stck_oprc") or item.get("open") or close or 0).replace(",", "")) if str(item.get("stck_oprc") or item.get("open") or close or 0).replace(",", "").strip() else close_val,
"high": float(str(item.get("stck_hgpr") or item.get("high") or close or 0).replace(",", "")) if str(item.get("stck_hgpr") or item.get("high") or close or 0).replace(",", "").strip() else close_val,
"low": float(str(item.get("stck_lwpr") or item.get("low") or close or 0).replace(",", "")) if str(item.get("stck_lwpr") or item.get("low") or close or 0).replace(",", "").strip() else close_val,
"volume": volume_val,
}
)
return rows
def fetch_price_history_kis(code: str, kis_account: str | None, benchmark_code: str | None = None) -> dict[str, Any]:
if not kis_account:
return {"status": "DATA_MISSING", "rows": []}
from src.quant_engine.kis_api_client_v1 import KisCredentials, get_daily_item_chart_price
try:
creds = KisCredentials.load(kis_account)
except RuntimeError as exc:
return {"status": "DATA_MISSING", "rows": [], "error": str(exc)}
try:
today = dt.date.today()
end = today.strftime("%Y%m%d")
start = (today - dt.timedelta(days=40)).strftime("%Y%m%d")
payload = get_daily_item_chart_price(creds, code, start, end, period="D")
rows = _parse_kis_price_rows(payload)
if benchmark_code is not None and not rows:
return {"status": "DATA_MISSING", "rows": []}
if rows:
return {
"status": "OK",
"rows": rows,
"source_url": "KIS Open API /uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice",
"source_as_of": _kst_now_iso(),
}
except Exception as exc: # noqa: BLE001
return {"status": "DATA_MISSING", "rows": [], "error": str(exc)}
return {"status": "DATA_MISSING", "rows": []}
def _fetch_price_bundle(
code: str,
*,
kis_account: str | None,
prefer_kis: bool = True,
) -> dict[str, Any]:
"""가격 히스토리와 벤치마크 히스토리를 동일 규칙으로 조립한다.
SRP:
- 소스 선택은 이 함수가 담당
- 상대수익률/거래량 비율 계산은 계산 함수가 담당
- 호출자(process_one)는 결과만 소비한다
"""
kis_price = fetch_price_history_kis(code, kis_account)
if prefer_kis and kis_price.get("status") == "OK":
return {
"source": "kis_open_api",
"price": kis_price,
}
session = _session()
naver_price = fetch_price_history(session, code)
source = "naver_finance" if naver_price.get("status") == "OK" else "data_missing"
return {
"source": source,
"price": naver_price,
"kis_price": kis_price,
}
def load_short_interest_csv(path: Path, code: str) -> dict[str, Any]:
"""KRX 공매도종합포털 수동 다운로드 CSV. 컬럼: 종목코드, 잔고율, 잔고율변화20일, 거래비중."""
import csv
@@ -145,12 +264,15 @@ def build_ctx_for_ticker(
external_context: dict[str, Any],
kis_account: str | None = None,
) -> dict[str, Any]:
session = _session()
price = fetch_price_history(session, code)
benchmark = fetch_price_history(session, benchmark_code)
price_bundle = _fetch_price_bundle(code, kis_account=kis_account, prefer_kis=True)
benchmark_bundle = _fetch_price_bundle(benchmark_code, kis_account=kis_account, prefer_kis=True)
price = price_bundle["price"]
benchmark = benchmark_bundle["price"]
relative_return_20d = compute_relative_return_20d(price.get("rows", []), benchmark.get("rows", []))
volume_ratio_5d = compute_volume_ratio_5d(price.get("rows", []))
price_rows = _coerce_price_rows(price.get("rows") or [])
benchmark_rows = _coerce_price_rows(benchmark.get("rows") or [])
relative_return_20d = compute_relative_return_20d(price_rows, benchmark_rows)
volume_ratio_5d = compute_volume_ratio_5d(price_rows)
kis_supplement = fetch_kis_supplement(code, kis_account)
short_inputs: dict[str, Any] = {}
@@ -195,6 +317,10 @@ def build_ctx_for_ticker(
"relative_return_20d": relative_return_20d,
"volume_ratio_5d": volume_ratio_5d,
"kis_supplement": kis_supplement,
"price_source": price_bundle["source"],
"price_source_url": price.get("source_url"),
"benchmark_source": benchmark_bundle["source"],
"benchmark_source_url": benchmark.get("source_url"),
"generated_at": _kst_now_iso(),
}