WBS-7.7: KIS수집→스냅샷→정성매도 E2E 통합테스트

단위 테스트는 모듈별로 충분했지만 KIS 수집→data_collection_store_v1.db
적재→정성매도전략 평가로 이어지는 실제 데이터 경로를 검증하는 테스트가
없었다. 네트워크를 전혀 사용하지 않고(no-naver/no-live-kis 경로, 또는
Naver 403 차단 모킹) 3단계 체인을 검증한다.
This commit is contained in:
2026-06-21 20:09:43 +09:00
parent 449721433b
commit 5568004704
@@ -0,0 +1,138 @@
"""WBS-7.7 — KIS 수집 → 스냅샷 어드민 적재 → 정성매도전략 평가 E2E 체인.
단위 테스트(tests/unit)는 각 모듈을 독립적으로 검증하지만, 모듈 간 실제 데이터
경로(kis_data_collection_v1 → data_collection_store_v1.db → snapshot_admin의
collection dashboard / qualitative_sell_strategy_v1 → qualitative_sell_strategy_store_v1.db)
를 연결해서 검증하는 테스트가 없었다(2026-06-21 비판적 리뷰 0c절, WBS-7.7).
이 테스트는 네트워크를 전혀 사용하지 않는다(--no-live-kis --no-naver와 동일한 경로,
또는 Naver 호출을 명시적으로 예외 처리시켜 graceful degradation을 검증).
"""
from __future__ import annotations
import json
import sys
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import pytest
from src.quant_engine import kis_data_collection_v1 as kdc
from src.quant_engine.data_collection_store_v1 import load_collection_dashboard_state
from src.quant_engine.qualitative_sell_strategy_v1 import compute_qualitative_sell_strategy
from src.quant_engine.qualitative_sell_strategy_store_v1 import (
fetch_recent_sell_strategy_results,
insert_sell_strategy_result,
)
SEED_ROWS = [
{"Ticker": "005930", "Name": "삼성전자", "Sector": "반도체"},
{"Ticker": "000660", "Name": "SK하이닉스", "Sector": "반도체"},
]
@pytest.fixture()
def seed_json(tmp_path: Path) -> Path:
path = tmp_path / "seed.json"
path.write_text(
json.dumps({"data": {"data_feed": SEED_ROWS}}, ensure_ascii=False),
encoding="utf-8",
)
return path
def test_kis_collection_writes_sqlite_that_snapshot_admin_dashboard_reads_back(tmp_path: Path, seed_json: Path):
"""1단계: KIS 수집(네트워크 미사용) → SQLite 적재 → snapshot_admin 대시보드 read-back."""
db_path = tmp_path / "data_collection_store_v1.db"
output_json = tmp_path / "kis_data_collection_v1.json"
summary = kdc.collect_to_sqlite(
input_json=seed_json,
sqlite_db=db_path,
output_json=output_json,
kis_account="mock",
include_naver=False,
include_live_kis=False,
)
assert summary["status"] in {"PASS", "PASS_WITH_WARNINGS"}
assert summary["row_count"] == len(SEED_ROWS)
assert not summary["errors"]
dashboard = load_collection_dashboard_state(db_path=db_path, output_json_path=output_json)
assert dashboard["counts"]["collection_runs"] >= 1
assert dashboard["counts"]["collection_snapshots"] == len(SEED_ROWS)
assert dashboard["counts"]["collection_source_errors"] == 0
tickers_in_dashboard = {row["ticker"] for row in dashboard["recent_snapshots"]}
assert {"005930", "000660"} <= tickers_in_dashboard
def test_naver_fetch_exception_degrades_gracefully_without_breaking_batch(tmp_path: Path, seed_json: Path, monkeypatch):
"""Cloudflare 403 등 Naver 폴백 차단 시 graceful degradation 검증 (spec/exit/qualitative_sell_strategy_v1.yaml:81-82 명시 리스크)."""
def _raise_cloudflare_block(_session, _code):
raise RuntimeError("HTTP 403 Forbidden (Cloudflare)")
monkeypatch.setattr(kdc, "fetch_price_history", _raise_cloudflare_block)
# naver_session/fetch_price_history may be None on environments without the optional
# dependency wired; force both non-None so _normalize_naver_price_history actually tries.
monkeypatch.setattr(kdc, "naver_session", lambda: object())
db_path = tmp_path / "data_collection_store_v1.db"
output_json = tmp_path / "kis_data_collection_v1.json"
summary = kdc.collect_to_sqlite(
input_json=seed_json,
sqlite_db=db_path,
output_json=output_json,
kis_account="mock",
include_naver=True,
include_live_kis=False,
)
# 배치 전체가 죽지 않고 끝까지 진행되어야 한다 — 개별 ticker의 naver 보강 실패는
# collection_source_errors가 아니라 정상 row로 (naver 필드 없이) 기록된다.
assert summary["status"] in {"PASS", "PASS_WITH_WARNINGS"}
assert summary["row_count"] == len(SEED_ROWS)
assert not summary["errors"], "Naver 차단은 개별 ticker 처리 중 흡수되어야 하며 배치 errors로 전파되면 안 된다"
def test_qualitative_sell_strategy_decision_round_trips_through_store(tmp_path: Path):
"""2단계: 정성매도전략 평가(순수 함수, 네트워크 미사용) → SQLite 저장 → 조회 round-trip."""
ctx = {
"today": date(2026, 6, 21),
"macro_pressure": 0.5,
"fundamental_trajectory": 0.4,
"short_interest_pressure": 0.6,
"microstructure_pressure": 0.2,
"liquidity_rotation_risk": 0.5,
"rate_trend": "RISING",
}
decision = compute_qualitative_sell_strategy(ctx)
assert decision["action"] in {
"EXIT_REVIEW_FULL",
"TRIM_REVIEW_PARTIAL",
"HOLD_ADD_CONVICTION",
"HOLD_NO_CONFLUENCE",
"INSUFFICIENT_DATA_NO_ACTION",
}
result = {
"code": "005930",
"generated_at": "2026-06-21T15:30:00+09:00",
"decision": decision,
}
db_path = tmp_path / "qualitative_sell_strategy.db"
insert_sell_strategy_result(db_path, result)
fetched = fetch_recent_sell_strategy_results(db_path, "005930", limit=5)
assert len(fetched) == 1
assert fetched[0]["code"] == "005930"
assert fetched[0]["action"] == decision["action"]
assert fetched[0]["conviction"] == decision["conviction"]
assert fetched[0]["market_regime"] == decision["market_regime"]