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 일일 스케줄(장마감 후) + 파이프라인 계약 검증 게이트
82 lines
3.0 KiB
Python
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
|