Files
QuantEngineByItz/tests/unit/test_evaluate_qualitative_sell_strategy_accuracy_v1.py
kjh2064 da0e1b0f7e 비기계적 매도전략(가치보존) + 위성종목 추천 엔진 추가
매크로·실적·펀더멘털·공매도수급·호가미시구조·대내외 변수 5개 독립
팩터군의 confluence(최소 3/5 합의) 없이는 매도 트리거를 금지하는
정성적 매도판단 엔진과, 보유종목 제외 위성후보 추천 로직을 추가한다.

- 단일 팩터 임계값 돌파만으로는 매도 신호를 생성하지 않음
  (mechanical_sell_prohibited=true)
- 데이터 결측 시 항상 DATA_MISSING/INSUFFICIENT_DATA_NO_ACTION —
  추정값으로 채우지 않음
- KIS 호가10단계·공매도거래비중 + Naver 시세/수급 스크래핑 입력 연동
- SQLite 시계열 저장 + 사후 적중률 자체평가
  (evaluate_qualitative_sell_strategy_accuracy_v1)
- Gitea 일일 스케줄(장마감 후) + 파이프라인 계약 검증 게이트
2026-06-21 20:05:55 +09:00

82 lines
3.0 KiB
Python

from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tools.evaluate_qualitative_sell_strategy_accuracy_v1 import (
_scoreable_direction,
build_accuracy_report,
evaluate_decision,
)
from src.quant_engine.qualitative_sell_strategy_store_v1 import insert_sell_strategy_result
def test_scoreable_direction():
assert _scoreable_direction("EXIT_REVIEW_FULL") == -1
assert _scoreable_direction("TRIM_REVIEW_PARTIAL") == -1
assert _scoreable_direction("HOLD_ADD_CONVICTION") == 1
assert _scoreable_direction("HOLD_NO_CONFLUENCE") is None
assert _scoreable_direction("INSUFFICIENT_DATA_NO_ACTION") is None
def test_evaluate_decision_sell_success_when_price_drops():
decision = {"action": "EXIT_REVIEW_FULL"}
result = evaluate_decision(decision, price_at_decision=100.0, price_after=90.0)
assert result["success"] is True
assert result["realized_return_pct"] == -10.0
def test_evaluate_decision_sell_failure_when_price_rises():
decision = {"action": "TRIM_REVIEW_PARTIAL"}
result = evaluate_decision(decision, price_at_decision=100.0, price_after=110.0)
assert result["success"] is False
def test_evaluate_decision_hold_add_success_when_price_rises():
decision = {"action": "HOLD_ADD_CONVICTION"}
result = evaluate_decision(decision, price_at_decision=100.0, price_after=105.0)
assert result["success"] is True
def test_evaluate_decision_returns_none_for_non_directional_action():
assert evaluate_decision({"action": "HOLD_NO_CONFLUENCE"}, 100.0, 105.0) is None
def test_build_accuracy_report_data_gated_when_sample_too_small(tmp_path):
db_path = tmp_path / "test.db"
insert_sell_strategy_result(db_path, {
"code": "005930", "generated_at": "2026-06-01T12:00:00",
"decision": {"action": "EXIT_REVIEW_FULL"},
})
report = build_accuracy_report(db_path, price_lookup={
"005930": {"2026-06-01": 100.0, "2026-06-06": 90.0},
})
assert report["status"] == "DATA_GATED"
assert report["scored_sample_count"] == 1
def test_build_accuracy_report_ok_with_enough_samples(tmp_path):
db_path = tmp_path / "test.db"
price_lookup: dict = {}
for i in range(12):
code = f"00000{i % 3}"
gen_at = f"2026-05-{(i % 20) + 1:02d}T12:00:00"
insert_sell_strategy_result(db_path, {
"code": code, "generated_at": gen_at,
"decision": {"action": "EXIT_REVIEW_FULL"},
})
date_key = gen_at[:10]
future_key = (
__import__("datetime").date.fromisoformat(date_key) + __import__("datetime").timedelta(days=5)
).isoformat()
price_lookup.setdefault(code, {})[date_key] = 100.0
price_lookup[code][future_key] = 90.0 # 매도신호 후 하락 — success
report = build_accuracy_report(db_path, price_lookup)
assert report["status"] == "OK"
assert report["hit_rate_pct"] == 100.0
assert report["scored_sample_count"] == 12