WBS-7.7: KIS수집→스냅샷→정성매도 E2E 통합테스트
단위 테스트는 모듈별로 충분했지만 KIS 수집→data_collection_store_v1.db 적재→정성매도전략 평가로 이어지는 실제 데이터 경로를 검증하는 테스트가 없었다. 네트워크를 전혀 사용하지 않고(no-naver/no-live-kis 경로, 또는 Naver 403 차단 모킹) 3단계 체인을 검증한다.
This commit is contained in:
@@ -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"]
|
||||
Reference in New Issue
Block a user