feat(kis-collection): finalize sqlite migration, add fallback resilience, and update WBS documentation
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user