Files
QuantEngineByItz/tests/unit/test_qualitative_sell_strategy_v1.py
T
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

149 lines
6.2 KiB
Python

from __future__ import annotations
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))
from src.quant_engine.qualitative_sell_strategy_v1 import (
classify_market_regime,
compute_microstructure_pressure_from_orderbook,
compute_qualitative_sell_strategy,
compute_satellite_candidate_score,
compute_short_interest_composite,
)
def test_classify_market_regime():
assert classify_market_regime("RISING") == "PERFORMANCE_MARKET"
assert classify_market_regime("FLAT") == "TECHNICAL_MARKET"
assert classify_market_regime("FALLING") == "TECHNICAL_MARKET"
assert classify_market_regime(None) == "NEUTRAL"
assert classify_market_regime("garbage") == "NEUTRAL"
def test_short_interest_composite_data_missing_without_estimating():
result = compute_short_interest_composite({"short_balance_ratio": 0.6})
assert result["status"] == "DATA_MISSING"
assert "short_turnover_share" in result["missing_inputs"]
assert result["short_interest_pressure"] is None
def test_short_interest_composite_low_balance_regime_reweights():
low_balance = compute_short_interest_composite({
"short_balance_ratio": 0.6, "short_balance_ratio_chg_20d": 0.1,
"short_turnover_share": 14.0, "relative_return_20d": -8.0,
"volume_ratio_5d": 1.8, "earnings_outlook": "DETERIORATING",
})
assert low_balance["low_balance_regime"] is True
assert low_balance["weights_used"]["balance"] < low_balance["weights_used"]["turnover"]
assert low_balance["label"] == "ELEVATED_SHORT_PRESSURE"
def test_confluence_requires_minimum_three_agreeing_factors():
# 2개 팩터만 매도방향(macro, short_interest) 합의 — 3개 미달이므로 매도 액션 금지
ctx = {
"macro_pressure": 0.5, "short_interest_pressure": 0.6,
"fundamental_trajectory": -0.5, "microstructure_pressure": -0.4,
"liquidity_rotation_risk": 0.1,
}
out = compute_qualitative_sell_strategy(ctx)
assert out["action"] not in {"EXIT_REVIEW_FULL", "TRIM_REVIEW_PARTIAL"}
def test_confluence_triggers_trim_when_three_factors_agree():
ctx = {
"macro_pressure": 0.5, "short_interest_pressure": 0.5,
"fundamental_trajectory": 0.4, "microstructure_pressure": 0.1,
"liquidity_rotation_risk": 0.0,
}
out = compute_qualitative_sell_strategy(ctx)
assert out["action"] == "TRIM_REVIEW_PARTIAL"
assert set(out["sell_agreeing_factors"]) == {"macro_pressure", "short_interest_pressure", "fundamental_trajectory"}
def test_insufficient_data_does_not_fabricate_action():
out = compute_qualitative_sell_strategy({"macro_pressure": 0.9})
assert out["action"] == "INSUFFICIENT_DATA_NO_ACTION"
assert out["mechanical_sell_prohibited"] is True
def test_review_window_pre_earnings_when_outlook_deteriorating():
ctx = {
"macro_pressure": 0.5, "fundamental_trajectory": 0.5, "short_interest_pressure": 0.5,
"earnings_outlook": "DETERIORATING",
"next_earnings_date": date(2026, 7, 24),
"today": date(2026, 6, 21),
}
out = compute_qualitative_sell_strategy(ctx)
assert out["review_window"]["window_basis"] == "PRE_EARNINGS_EXIT_BEFORE_SURPRISE_RISK"
assert out["review_window"]["review_window_end"] < "2026-07-24"
def test_review_window_defers_past_earnings_when_outlook_improving():
ctx = {
"macro_pressure": -0.5, "fundamental_trajectory": -0.5, "short_interest_pressure": -0.5,
"earnings_outlook": "IMPROVING",
"next_earnings_date": date(2026, 7, 24),
"today": date(2026, 6, 21),
}
out = compute_qualitative_sell_strategy(ctx)
assert out["action"] == "HOLD_ADD_CONVICTION"
assert out["review_window"]["window_basis"] == "REASSESS_AFTER_EARNINGS_CONFIRM"
def test_regime_weighting_shifts_composite_score_without_changing_confluence_count():
base_ctx = {
"macro_pressure": 0.4, "fundamental_trajectory": 0.6, "short_interest_pressure": 0.35,
"microstructure_pressure": 0.1, "liquidity_rotation_risk": 0.0,
}
performance = compute_qualitative_sell_strategy({**base_ctx, "rate_trend": "RISING"})
technical = compute_qualitative_sell_strategy({**base_ctx, "rate_trend": "FALLING"})
assert performance["market_regime"] == "PERFORMANCE_MARKET"
assert technical["market_regime"] == "TECHNICAL_MARKET"
assert performance["sell_agreeing_factors"] == technical["sell_agreeing_factors"]
assert performance["composite_score"] != technical["composite_score"]
def test_satellite_candidate_score_insufficient_data():
out = compute_satellite_candidate_score({"fundamental_trajectory": 0.2})
assert out["satellite_action"] == "INSUFFICIENT_DATA_NO_ACTION"
def test_satellite_candidate_score_buy_candidate_on_strong_export_and_fundamentals():
out = compute_satellite_candidate_score({
"sector_export_trend": 12.0, "fundamental_trajectory": -0.4,
"relative_return_20d": 3.0, "rate_trend": "RISING",
})
assert out["satellite_action"] == "BUY_CANDIDATE"
assert out["market_regime"] == "PERFORMANCE_MARKET"
def test_microstructure_pressure_from_orderbook_ask_heavy_is_positive():
out = compute_microstructure_pressure_from_orderbook({"total_askp_rsqn": "300000", "total_bidp_rsqn": "100000"})
assert out["status"] == "OK"
assert out["microstructure_pressure"] > 0
def test_microstructure_pressure_from_orderbook_bid_heavy_is_negative():
out = compute_microstructure_pressure_from_orderbook({"total_askp_rsqn": "100000", "total_bidp_rsqn": "300000"})
assert out["microstructure_pressure"] < 0
def test_microstructure_pressure_from_orderbook_missing_fields():
out = compute_microstructure_pressure_from_orderbook({})
assert out["status"] == "DATA_MISSING"
assert out["microstructure_pressure"] is None
def test_map_universe_sector_to_hs_sector_substring_match():
from tools.build_satellite_candidate_recommendations_v1 import map_universe_sector_to_hs_sector
assert map_universe_sector_to_hs_sector("반도체/PCB") == "반도체"
assert map_universe_sector_to_hs_sector("자동차/부품") == "자동차"
assert map_universe_sector_to_hs_sector("AI전력/기기") is None
assert map_universe_sector_to_hs_sector(None) is None