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