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