- 스윙 체결가 확인 전 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
87 lines
3.5 KiB
Python
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
|