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
This commit is contained in:
@@ -883,6 +883,75 @@ class TestCheckExitSignal:
|
||||
assert result.action == "SELL_ALL"
|
||||
assert "손절" in result.reason
|
||||
|
||||
# ── R4 2026-10-09: 1단계(분할 전)에서 전량 조건은 50% 분할로 처리된다 ──────
|
||||
# 문서(docstring)가 "단계 무관 전량"이라 서술했으나 실제로는 분할 분기가 먼저 return 한다.
|
||||
# 동작은 유지(백테스트 없이 청산 타이밍 변경 금지 — §8.1-5)하고 계약을 테스트로 고정한다.
|
||||
# 적응형 오버레이는 예외 발생형 목으로 끊어 config(settings) 임계를 쓰게 한다.
|
||||
|
||||
@staticmethod
|
||||
def _block_adaptive(monkeypatch):
|
||||
import app.services.adaptive_strategy as _adp_mod
|
||||
|
||||
def _raise(*_a, **_k):
|
||||
raise RuntimeError("adaptive blocked in test")
|
||||
|
||||
monkeypatch.setattr(_adp_mod, "get_current_adaptive_params", _raise)
|
||||
|
||||
@staticmethod
|
||||
def _full_exit_kwargs(remaining_pct):
|
||||
from app.core.config import settings
|
||||
entry = 10000.0
|
||||
bb_upper = 10500.0
|
||||
# BB상단×임계 이상 + RSI ≥ 전량 임계 → 전량 조건 2개 모두 성립. 수익 ≥ 0.5%.
|
||||
close = bb_upper * settings.MEAN_REVERSION_BB_UPPER_THRESHOLD + 10
|
||||
return dict(
|
||||
regime=MarketRegime.BULL,
|
||||
rsi=settings.MEAN_REVERSION_RSI_FULL_EXIT + 1.0,
|
||||
close=close,
|
||||
bb_upper=bb_upper,
|
||||
bb_middle=10100.0,
|
||||
bb_lower=9500.0,
|
||||
entry_price=entry,
|
||||
atr=50.0, # 손절선(≥ entry−2.5×ATR) 훨씬 아래 → 손절 미발동
|
||||
lowest_since_entry=9900.0,
|
||||
remaining_pct=remaining_pct,
|
||||
)
|
||||
|
||||
def test_full_exit_conditions_before_partial_are_split_r4(self, monkeypatch):
|
||||
"""remaining_pct=1.0 + 전량 조건(RSI≥full·BB상단) → SELL_PARTIAL 50% (현재 계약)"""
|
||||
self._block_adaptive(monkeypatch)
|
||||
kw = self._full_exit_kwargs(remaining_pct=1.0)
|
||||
assert (kw["close"] - kw["entry_price"]) / kw["entry_price"] * 100 >= 0.5
|
||||
result = check_exit_signal(**kw)
|
||||
assert result is not None
|
||||
assert result.action == "SELL_PARTIAL"
|
||||
assert result.sell_pct == 0.50
|
||||
|
||||
def test_full_exit_conditions_after_partial_sell_all_r4(self, monkeypatch):
|
||||
"""remaining_pct=0.50(1차 익절 후) + 같은 조건 → SELL_ALL, 사유에 RSI·BB상단 모두"""
|
||||
self._block_adaptive(monkeypatch)
|
||||
result = check_exit_signal(**self._full_exit_kwargs(remaining_pct=0.50))
|
||||
assert result is not None
|
||||
assert result.action == "SELL_ALL"
|
||||
assert result.sell_pct == 1.0
|
||||
assert "RSI(" in result.reason and "BB상단" in result.reason
|
||||
|
||||
def test_full_exit_before_partial_below_min_profit_holds_r6(self, monkeypatch):
|
||||
"""[R6 2026-10-10] 수익 < 0.5% 이면 RSI/BB상단 전량 '익절'도 나가지 않는다.
|
||||
(R4 판은 이 구간 SELL_ALL 을 고정했으나, 손실 구간 '익절'이 연패로 쌓이는 결함이었다.)
|
||||
손실 구간은 손절·시간손절이 맡는다."""
|
||||
self._block_adaptive(monkeypatch)
|
||||
kw = self._full_exit_kwargs(remaining_pct=1.0)
|
||||
kw["entry_price"] = kw["close"] # 수익 0% → 분할·전량 익절 모두 skip
|
||||
kw["lowest_since_entry"] = kw["close"] - 10
|
||||
assert check_exit_signal(**kw) is None
|
||||
|
||||
def test_docstring_no_longer_claims_stage_independent_r4(self):
|
||||
"""docstring 이 '단계 무관 전량'을 다시 주장하지 않도록 고정"""
|
||||
doc = check_exit_signal.__doc__ or ""
|
||||
assert "도달 (단계 무관)" not in doc
|
||||
assert "단계 우선순위" in doc
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# 9. calculate_position_size
|
||||
|
||||
Reference in New Issue
Block a user