"""§8.5-39: SWING_STOP_IGNORE_CRISIS_OVERLAY — Crisis 모드가 스윙 손절을 소급 조정하지 않는다.""" from types import SimpleNamespace import pytest from app.core.config import settings from app.services import mean_reversion_strategy as mr class _NoCfg: def get(self, *a, **k): return None def _adp(mode, sl, sl_hv, mult, fallback=False): return SimpleNamespace(trading_mode=mode, stop_loss_pct=sl, stop_loss_high_vol=sl_hv, atr_stop_multiplier=mult, fallback_used=fallback) @pytest.fixture def patch_env(monkeypatch): monkeypatch.setattr(mr, "strategy_cfg", _NoCfg()) def _set(adp, flag): import app.services.adaptive_strategy as ad monkeypatch.setattr(ad, "get_current_adaptive_params", lambda: adp) monkeypatch.setattr(settings, "SWING_STOP_IGNORE_CRISIS_OVERLAY", flag, raising=False) return _set CRISIS = lambda: _adp("crisis", -3.0, -5.0, 1.5) # ATR/close: 저변동 1%, 고변동 5% LOW, HIGH = (1.0, 100.0), (5.0, 100.0) def test_default_flag_is_false(): assert settings.SWING_STOP_IGNORE_CRISIS_OVERLAY is False def test_flag_off_crisis_overlay_still_applies(patch_env): patch_env(CRISIS(), False) assert mr.resolve_swing_stop_params_v2(*LOW) == (-3.0, 1.5) assert mr.resolve_swing_stop_params_v2(*HIGH) == (-5.0, 1.5) def test_flag_on_crisis_uses_config(patch_env): patch_env(CRISIS(), True) assert mr.resolve_swing_stop_params_v2(*LOW) == (-abs(float(settings.STOP_LOSS_PERCENT)), float(settings.ATR_STOP_MULTIPLIER)) assert mr.resolve_swing_stop_params_v2(*HIGH) == (-abs(float(settings.STOP_LOSS_HIGH_VOL)), float(settings.ATR_STOP_MULTIPLIER)) @pytest.mark.parametrize("flag", [False, True]) @pytest.mark.parametrize("mode,sl,hv,m", [("normal", -7.0, -11.0, 2.5), ("defensive", -6.0, -8.0, 2.0), ("aggressive", -9.0, -12.0, 3.0)]) def test_other_modes_unchanged_by_flag(patch_env, flag, mode, sl, hv, m): patch_env(_adp(mode, sl, hv, m), flag) assert mr.resolve_swing_stop_params_v2(*LOW) == (sl, m) assert mr.resolve_swing_stop_params_v2(*HIGH) == (hv, m) def test_flag_on_crisis_respects_strategy_cfg(patch_env, monkeypatch): class Cfg: def get(self, k, *a): return {"STOP_LOSS_PERCENT": -6.5, "ATR_STOP_MULTIPLIER": 2.2}.get(k) patch_env(CRISIS(), True) monkeypatch.setattr(mr, "strategy_cfg", Cfg()) assert mr.resolve_swing_stop_params_v2(*LOW) == (-6.5, 2.2) def test_sign_guard_kept_with_flag_on(patch_env, monkeypatch): class Cfg: def get(self, k, *a): return 6.0 if k == "STOP_LOSS_PERCENT" else None patch_env(CRISIS(), True) monkeypatch.setattr(mr, "strategy_cfg", Cfg()) assert mr.resolve_swing_stop_params_v2(*LOW)[0] == -6.0 def test_flag_on_fallback_used_adaptive_ignored_as_before(patch_env): patch_env(_adp("crisis", -3.0, -5.0, 1.5, fallback=True), True) assert mr.resolve_swing_stop_params_v2(*LOW)[0] == -abs(float(settings.STOP_LOSS_PERCENT))