Files
auto-stock/backend/tests/test_kr_backtest_exit_v2_20261009.py
shuim a264561576 fix: 하드닝 루프 8회차 — 체결 보정·고아 감지·청산 경보·실시간/차트·리스크 가드 일괄 수정
- 스윙 체결가 확인 전 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
2026-10-10 12:17:49 +09:00

87 lines
3.5 KiB
Python

"""kr_backtest_engine 청산: 라이브 모드/DB 비의존 + 실전 분할익절 규약."""
from datetime import date
import pytest
from app.core.config import settings
from app.services import kr_backtest_engine as eng
from app.services.kr_backtest_engine import (
BarData,
MeanReversionBacktest,
Position,
check_exit_signal_backtest_v2,
)
from app.services.sizing_core import remaining_pct_after_partial_sell
@pytest.fixture(autouse=True)
def _no_live_state(monkeypatch):
"""라이브 적응형·DB 값 조회는 호출 즉시 예외 (거짓 통과 방지)."""
def boom(*a, **k):
raise AssertionError("백테스트 청산이 라이브 상태를 읽음")
import app.services.adaptive_strategy as adp
import app.services.mean_reversion_strategy as mr
monkeypatch.setattr(adp, "get_current_adaptive_params", boom)
monkeypatch.setattr(mr, "resolve_swing_stop_params_v2", boom)
monkeypatch.setattr(eng, "check_exit_signal", boom)
def _p(**kw):
return {**MeanReversionBacktest().default_params(), **kw}
def test_stop_uses_params():
p = _p(stop_loss_pct=3.0, atr_stop_multiplier=settings.SWING_ATR_STOP_MULT_MAX)
sig = check_exit_signal_backtest_v2(p, 50, 9690, 20000, 20000, 10000, 100, 1.0, None)
assert sig and sig.sell_pct == 1.0 and "손절" in sig.reason
assert check_exit_signal_backtest_v2(p, 50, 9750, 20000, 20000, 10000, 100, 1.0, None) is None
def test_positive_stop_sign_guard():
sig = check_exit_signal_backtest_v2(_p(stop_loss_pct=7.0), 50, 10100, 20000, 20000, 10000, 100, 1.0, None)
assert sig is None # 부호가 뒤집혀 +7% 로 해석되면 손절이 발동한다
def test_rsi_partial_threshold_from_params():
# close 10100 < bb_middle, 2xATR 미달 -> RSI 만 판정
args = (41, 10100, 20000, 20000, 10000, 100, 1.0, None)
sig = check_exit_signal_backtest_v2(_p(rsi_exit_threshold=40), *args)
assert sig and sig.action == "SELL_PARTIAL" and sig.sell_pct == 0.5
assert check_exit_signal_backtest_v2(_p(rsi_exit_threshold=60), *args) is None
def _bars(n, price):
return [BarData(trade_date=date(2024, 1, 1), open_price=price, high_price=price + 5,
low_price=price - 5, close_price=price, volume=1000, stock_code="A")
for _ in range(n)]
@pytest.fixture
def _ind(monkeypatch):
monkeypatch.setattr(eng, "calculate_rsi", lambda *a, **k: float(settings.MEAN_REVERSION_RSI_EXIT_THRESHOLD) + 1)
monkeypatch.setattr(eng, "calculate_bollinger", lambda *a, **k: (20000.0, 20000.0, 8000.0))
monkeypatch.setattr(eng, "calculate_atr", lambda *a, **k: 10.0)
def _pos(qty, orig, rem):
return Position("A", date(2024, 1, 1), 10000.0, qty, 10100.0, remaining_pct=rem, original_quantity=orig)
def test_partial_sell_qty_from_original_and_small_full(_ind):
st = MeanReversionBacktest()
bars = {"A": _bars(80, 10100)}
o = st.on_bar(70, bars, {"A": _pos(7, 7, 1.0)}, 0, 1e7, _p(max_positions=1))
assert o and o[0]["quantity"] == 3
# 단계 소진(0.5) 이후엔 1차 익절 재발동 없음
assert st.on_bar(70, bars, {"A": _pos(4, 7, 0.5)}, 0, 1e7, _p(max_positions=1)) == []
# 잔량 3주 이하 + 신호 -> 전량
o = st.on_bar(70, bars, {"A": _pos(3, 7, 1.0)}, 0, 1e7, _p(max_positions=1))
assert o[0]["quantity"] == 3
def test_stage_remaining_pct_blocks_second_partial():
r = remaining_pct_after_partial_sell(1.0, 0.5, 4, 7)
assert r <= 0.5
args = (settings.MEAN_REVERSION_RSI_EXIT_THRESHOLD + 1, 10100, 20000, 20000, 10000, 100, r, None)
assert check_exit_signal_backtest_v2(_p(), *args) is None