#!/usr/bin/env python3 import sys import json import yaml from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src.quant_engine.exit_decisions import ( compute_stop_price_core, compute_stop_action_ladder, compute_dynamic_heat_thresholds, ) from src.quant_engine.compute_formula_outputs import ( compute_imputed_data_exposure, compute_cash_recovery_optimizer, krx_tick_unit, ) def test_cash_shortfall_monotonicity(): # 1. Cash Shortfall Monotonicity: 현금 부족액이 증가하면 매도 계획의 예상 회수액과 대상 종목 수는 단조 증가해야 함. sell_candidates = [ {"Ticker": "005930", "Name": "삼성전자", "Sell_Qty": 100, "Sell_Limit_Price": 70000, "Cash_Preserve_Ratio": 100, "Cash_Preserve_Style": "FULL"}, {"Ticker": "000660", "Name": "SK하이닉스", "Sell_Qty": 50, "Sell_Limit_Price": 180000, "Cash_Preserve_Ratio": 100, "Cash_Preserve_Style": "FULL"}, ] res_small = compute_cash_recovery_optimizer(sell_candidates, 1_000_000) res_large = compute_cash_recovery_optimizer(sell_candidates, 10_000_000) seq_small = res_small["cash_recovery_plan_json"]["sell_sequence"] seq_large = res_large["cash_recovery_plan_json"]["sell_sequence"] assert len(seq_large) >= len(seq_small), "Item count should not decrease when shortfall increases" assert res_large["cash_recovery_plan_json"]["expected_total_krw"] >= res_small["cash_recovery_plan_json"]["expected_total_krw"], "Expected recovered cash should not decrease when shortfall increases" print("[PASS] INV_CASH_SHORTFALL_MONOTONICITY") def test_market_risk_monotonicity(): # 2. Market Risk Monotonicity: regime이 RISK_OFF 일 때 max position count 등 제약이 강화되는지 검증 # GatherTradingData.json 의 settings 구조 확인 json_path = ROOT / "GatherTradingData.json" if json_path.exists(): raw = json.loads(json_path.read_text(encoding="utf-8")) settings = raw.get("data", {}).get("settings", {}) pos_normal = settings.get("position_count_max_normal", 12) pos_risk_off = settings.get("position_count_max_risk_off", 8) assert pos_risk_off < pos_normal, "Risk off limit must be strictly more conservative than normal limit" print("[PASS] INV_MARKET_RISK_MONOTONICITY") def test_missing_data_confidence(): # 3. Missing Data Confidence: domain coverage가 낮아지면 weighted coverage가 하락하고 imputed field ratio(ifr)가 상승하여 confidence가 낮아져야 함. coverage_high = {"fundamental_core": 1.0, "realized_outcome": 1.0, "trade_quality": 1.0, "pattern": 1.0, "alpha_eval": 1.0} coverage_low = {"fundamental_core": 0.5, "realized_outcome": 0.5, "trade_quality": 0.5, "pattern": 0.5, "alpha_eval": 0.5} res_high = compute_imputed_data_exposure(coverage_high, 100.0) res_low = compute_imputed_data_exposure(coverage_low, 100.0) assert res_low["weighted_coverage"] < res_high["weighted_coverage"], "Weighted coverage must drop" assert res_low["imputed_field_ratio"] > res_high["imputed_field_ratio"], "Imputed field ratio must rise" assert res_low["effective_confidence_honest"] < res_high["effective_confidence_honest"], "Confidence must drop" print("[PASS] INV_MISSING_DATA_CONFIDENCE") def test_stale_price_zero_quantity(): # 4. Stale Price / Data Missing: 필수 데이터 결측 시 stop price 계산이 PASS가 되지 못하고 DATA_MISSING 경고가 되며 fallback 로직이 작동하는지 검증 res_missing = compute_stop_price_core(entry_price=10000.0, atr20=None, current_price=10000.0) assert res_missing["stop_price_status"].startswith("DATA_MISSING"), "Missing ATR must trigger DATA_MISSING" assert res_missing["stop_price"] == 10000.0 * 0.92, "Fallback stop price must be 92% of entry price" print("[PASS] INV_STALE_PRICE_ZERO_QUANTITY") def main(): try: test_cash_shortfall_monotonicity() test_market_risk_monotonicity() test_missing_data_confidence() test_stale_price_zero_quantity() except AssertionError as e: print(f"[FAIL] Invariant check failed: {e}") sys.exit(1) result_path = ROOT / "Temp" / "property_test_result_v1.json" result_path.parent.mkdir(parents=True, exist_ok=True) result_path.write_text(json.dumps({ "status": "PASS", "tests_run": 4, "timestamp": "2026-06-07T15:00:00Z" }, indent=2), encoding="utf-8") print(f"Property tests completed successfully. Results saved to {result_path}") sys.exit(0) if __name__ == "__main__": main()