"""하드닝 루프 R2 (2026-10-09) — mean_reversion_strategy BEAR v2 회귀 테스트. 1. BEAR v2 진입 description 에 f-string `%%B` 리터럴이 남아 저널·ML 텍스트에 '%%B' 로 기록되던 결함. 2. MEAN_REVERSION_BEAR_V2_ENABLED 가 실전 진입 경로에 배선되어 있지 않다는 사실(백테스트 전용)을 고정한다. 누군가 실전 경로에 배선하면 이 테스트가 깨져 문서(docstring·config·CLAUDE.md §6.1) 갱신을 강제한다. """ import ast import os import re from unittest.mock import MagicMock, patch import pytest from app.core.config import settings import app.services.mean_reversion_strategy as mrs BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @pytest.fixture(autouse=True) def _cfg_none(): """strategy_cfg(DB) 를 비워 settings 값으로 폴백시킨다(테스트 시점 바인딩).""" _m = MagicMock() _m.get.side_effect = lambda key, default=None: None with patch.object(mrs, "strategy_cfg", _m): yield def _full_score_signal(): bear_thr = settings.MEAN_REVERSION_BEAR_RSI_THRESHOLD rsi = min(bear_thr, settings.MEAN_REVERSION_RSI_STRONG_THRESHOLD) - 5.0 avg_vol = 1_000_000 cur_vol = int(avg_vol * (settings.MEAN_REVERSION_BEAR_VOLUME_MULT + 1.0)) return mrs.check_bear_buy_signal_v2( rsi=rsi, close=10_100.0, # %B = 0.05 bb_upper=12_000.0, bb_lower=10_000.0, avg_volume_20d=avg_vol, current_volume=cur_vol, is_reversal=True, rsi_prev=rsi - 2.0, bear_rsi_threshold=bear_thr, ) def test_bear_v2_description_has_single_percent_b(): sig = _full_score_signal() assert sig is not None, "만점 입력에서 BEAR v2 신호가 나야 한다" desc = sig["description"] assert "%%B" not in desc assert re.search(r"\+ %B\(0\.05/", desc), desc def _calls_in(path, names): with open(path, encoding="utf-8") as f: tree = ast.parse(f.read()) hits = [] for node in ast.walk(tree): if isinstance(node, ast.Call): fn = node.func nm = fn.id if isinstance(fn, ast.Name) else (fn.attr if isinstance(fn, ast.Attribute) else None) if nm in names: hits.append(nm) return hits def test_bear_v2_is_backtest_only(): """check_buy_signal / check_bear_buy_signal_v2 의 실전 호출부가 없음을 고정한다.""" allowed = { os.path.join(BACKEND_DIR, "app", "services", "mean_reversion_strategy.py"), os.path.join(BACKEND_DIR, "app", "services", "kr_backtest_engine.py"), os.path.join(BACKEND_DIR, "run_backtest.py"), } names = {"check_buy_signal", "check_bear_buy_signal_v2", "_bear_v2_enabled"} offenders = [] for root, dirs, files in os.walk(BACKEND_DIR): dirs[:] = [d for d in dirs if d not in ("tests", ".venv", "venv", "__pycache__", "node_modules")] for fn in files: if not fn.endswith(".py"): continue p = os.path.join(root, fn) if p in allowed: continue try: hits = _calls_in(p, names) except SyntaxError: continue if hits: offenders.append((os.path.relpath(p, BACKEND_DIR), hits)) assert not offenders, ( f"BEAR v2/check_buy_signal 이 실전 경로에 배선됨: {offenders} — " "mean_reversion_strategy docstring·config 주석·CLAUDE.md §6.1 '백테스트 전용' 서술을 갱신할 것" ) # 문서가 사실과 일치하는지 assert "백테스트 전용" in (mrs.check_bear_buy_signal_v2.__doc__ or "") def test_recommendations_bear_branch_ignores_v2_flag(): p = os.path.join(BACKEND_DIR, "app", "api", "endpoints", "recommendations.py") with open(p, encoding="utf-8") as f: src = f.read() assert "MEAN_REVERSION_BEAR_V2" not in src assert "check_bear_buy_signal_v2" not in src