From 556800470470217248129cf1aadbc114eed012db Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 21 Jun 2026 20:09:43 +0900 Subject: [PATCH] =?UTF-8?q?WBS-7.7:=20KIS=EC=88=98=EC=A7=91=E2=86=92?= =?UTF-8?q?=EC=8A=A4=EB=83=85=EC=83=B7=E2=86=92=EC=A0=95=EC=84=B1=EB=A7=A4?= =?UTF-8?q?=EB=8F=84=20E2E=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단위 테스트는 모듈별로 충분했지만 KIS 수집→data_collection_store_v1.db 적재→정성매도전략 평가로 이어지는 실제 데이터 경로를 검증하는 테스트가 없었다. 네트워크를 전혀 사용하지 않고(no-naver/no-live-kis 경로, 또는 Naver 403 차단 모킹) 3단계 체인을 검증한다. --- ..._to_snapshot_admin_and_sell_strategy_v1.py | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/integration/test_kis_collection_to_snapshot_admin_and_sell_strategy_v1.py diff --git a/tests/integration/test_kis_collection_to_snapshot_admin_and_sell_strategy_v1.py b/tests/integration/test_kis_collection_to_snapshot_admin_and_sell_strategy_v1.py new file mode 100644 index 0000000..32c8365 --- /dev/null +++ b/tests/integration/test_kis_collection_to_snapshot_admin_and_sell_strategy_v1.py @@ -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"]