"""walk_forward: 실전 청산 모델(kr_backtest_engine)을 돌리고, 엔진이 소비하는 키만 탐색한다.""" import asyncio from datetime import date import pytest from app.core.config import settings from app.services import walk_forward as wf from app.services.kr_backtest_engine import ( BacktestMetrics, BacktestResult, KrBacktestEngine, MeanReversionBacktest, ) class _Trial: def __init__(self): self.suggested = [] def suggest_int(self, name, lo, hi): self.suggested.append(name) return lo def suggest_float(self, name, lo, hi, step=None): self.suggested.append(name) return lo def _cfg(**kw): return wf.WalkForwardConfig(stock_codes=["005930"], start_date=date(2025, 1, 1), end_date=date(2025, 12, 31), **kw) @pytest.fixture(autouse=True) def _block_toy_engine(monkeypatch): """구 장난감 엔진 호출은 즉시 예외 (거짓 통과 방지).""" import app.services.backtest_engine as toy def boom(*a, **k): raise AssertionError("장난감 backtest_engine 사용") monkeypatch.setattr(toy.BacktestEngine, "run_backtest", boom) def test_search_space_only_engine_consumed_keys(): t = _Trial() params = wf.WalkForwardOptimizer()._build_params_from_trial_v2(t) consumed = set(MeanReversionBacktest().default_params()) assert set(params) <= consumed, set(params) - consumed assert "MEAN_REVERSION_RSI_THRESHOLD" not in params assert params["stop_loss_pct"] > 0 def test_run_uses_kr_engine_and_normalizes_units(monkeypatch): seen = {} async def fake_run(self, **kw): seen.update(kw) return BacktestResult(metrics=BacktestMetrics( total_return_pct=10.0, max_drawdown_pct=-5.0, win_rate=60.0, sharpe_ratio=1.2, total_trades=7)) monkeypatch.setattr(KrBacktestEngine, "run", fake_run) out = asyncio.run(wf.WalkForwardOptimizer()._run_single_backtest_v2( ["005930"], date(2025, 1, 1), date(2025, 6, 1), {"bb_period": 20}, _cfg(position_size=0.5, max_positions=99))) assert out["mdd_pct"] == pytest.approx(-0.05) assert out["win_rate"] == pytest.approx(0.6) assert out["total_return_pct"] == pytest.approx(0.10) assert out["total_trades"] == 7 assert seen["params"]["position_size_pct"] <= settings.MAX_POSITION_SIZE assert seen["params"]["max_positions"] <= settings.MAX_HOLDINGS_COUNT def test_default_params_from_settings(): d = wf.WalkForwardOptimizer._default_params_v2() assert d["stop_loss_pct"] == abs(settings.STOP_LOSS_PERCENT) assert d["atr_stop_multiplier"] == settings.ATR_STOP_MULTIPLIER def test_recommended_params_keys(): wr = wf.WindowResult(0, "a", "b", "c", "d", {"bb_period": 18, "atr_stop_multiplier": 2.0, "stop_loss_pct": 5.0}, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, False) rec = wf.WalkForwardOptimizer()._compute_recommended_params([wr]) assert rec == {"bb_period": 18, "atr_stop_multiplier": 2.0, "stop_loss_pct": 5.0}