- 스윙 체결가 확인 전 executed_price NULL + fill_sweep, 진입가 resolve(ActiveTrade↔Portfolio), 0체결/부분체결 매수 장부 원복 - DT 고아 감지(근거 한정)·0체결 매도 복원·stale 점검 확대, 시간손절 -0.3% 원복 - 폴백 손절 ATR 캐시, 거래량부족-차단 신호 매수 차단, 현금 5% 유보 재적용, MDD peak/reset 수정 - KIS 서킷브레이커 청산 우회·경보, db_worker 재큐잉 순서, heartbeat·health-watch 사이드카 - 실시간 WS 재구독·41건 우선순위·다건 프레임 분할, 차트 샘플 제거·당일봉 합성, WS 서브프로토콜 인증 - 프론트: 미관리 그룹·확인 모달·차트 실시간 반영·예수금 표시 정정 - 전문가 에이전트 6종 추가(.claude/agents) - 유닛 스위트 2667 passed (소켓 차단 하), tsc OK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y83Za98XsNw9zuA4FTUytN
83 lines
3.0 KiB
Python
83 lines
3.0 KiB
Python
"""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}
|