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:
2026-10-10 12:17:49 +09:00
parent 5c4803b339
commit a264561576
230 changed files with 41160 additions and 2334 deletions

View File

@@ -0,0 +1,105 @@
"""하드닝 루프 R2 (2026-10-09) — mean_reversion_strategy BEAR v2 회귀 테스트.
1. BEAR v2 진입 description 에 f-string `%%B` 리터럴이 남아 저널·ML 텍스트에 '%%B' 로 기록되던 결함.
2. MEAN_REVERSION_BEAR_V2_ENABLED 가 실전 진입 경로에 배선되어 있지 않다는 사실(백테스트 전용)을
고정한다. 누군가 실전 경로에 배선하면 이 테스트가 깨져 문서(docstring·config·CLAUDE.md §6.1)
갱신을 강제한다.
"""
import ast
import os
import re
from unittest.mock import MagicMock, patch
import pytest
from app.core.config import settings
import app.services.mean_reversion_strategy as mrs
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@pytest.fixture(autouse=True)
def _cfg_none():
"""strategy_cfg(DB) 를 비워 settings 값으로 폴백시킨다(테스트 시점 바인딩)."""
_m = MagicMock()
_m.get.side_effect = lambda key, default=None: None
with patch.object(mrs, "strategy_cfg", _m):
yield
def _full_score_signal():
bear_thr = settings.MEAN_REVERSION_BEAR_RSI_THRESHOLD
rsi = min(bear_thr, settings.MEAN_REVERSION_RSI_STRONG_THRESHOLD) - 5.0
avg_vol = 1_000_000
cur_vol = int(avg_vol * (settings.MEAN_REVERSION_BEAR_VOLUME_MULT + 1.0))
return mrs.check_bear_buy_signal_v2(
rsi=rsi,
close=10_100.0, # %B = 0.05
bb_upper=12_000.0,
bb_lower=10_000.0,
avg_volume_20d=avg_vol,
current_volume=cur_vol,
is_reversal=True,
rsi_prev=rsi - 2.0,
bear_rsi_threshold=bear_thr,
)
def test_bear_v2_description_has_single_percent_b():
sig = _full_score_signal()
assert sig is not None, "만점 입력에서 BEAR v2 신호가 나야 한다"
desc = sig["description"]
assert "%%B" not in desc
assert re.search(r"\+ %B\(0\.05/", desc), desc
def _calls_in(path, names):
with open(path, encoding="utf-8") as f:
tree = ast.parse(f.read())
hits = []
for node in ast.walk(tree):
if isinstance(node, ast.Call):
fn = node.func
nm = fn.id if isinstance(fn, ast.Name) else (fn.attr if isinstance(fn, ast.Attribute) else None)
if nm in names:
hits.append(nm)
return hits
def test_bear_v2_is_backtest_only():
"""check_buy_signal / check_bear_buy_signal_v2 의 실전 호출부가 없음을 고정한다."""
allowed = {
os.path.join(BACKEND_DIR, "app", "services", "mean_reversion_strategy.py"),
os.path.join(BACKEND_DIR, "app", "services", "kr_backtest_engine.py"),
os.path.join(BACKEND_DIR, "run_backtest.py"),
}
names = {"check_buy_signal", "check_bear_buy_signal_v2", "_bear_v2_enabled"}
offenders = []
for root, dirs, files in os.walk(BACKEND_DIR):
dirs[:] = [d for d in dirs if d not in ("tests", ".venv", "venv", "__pycache__", "node_modules")]
for fn in files:
if not fn.endswith(".py"):
continue
p = os.path.join(root, fn)
if p in allowed:
continue
try:
hits = _calls_in(p, names)
except SyntaxError:
continue
if hits:
offenders.append((os.path.relpath(p, BACKEND_DIR), hits))
assert not offenders, (
f"BEAR v2/check_buy_signal 이 실전 경로에 배선됨: {offenders} — "
"mean_reversion_strategy docstring·config 주석·CLAUDE.md §6.1 '백테스트 전용' 서술을 갱신할 것"
)
# 문서가 사실과 일치하는지
assert "백테스트 전용" in (mrs.check_bear_buy_signal_v2.__doc__ or "")
def test_recommendations_bear_branch_ignores_v2_flag():
p = os.path.join(BACKEND_DIR, "app", "api", "endpoints", "recommendations.py")
with open(p, encoding="utf-8") as f:
src = f.read()
assert "MEAN_REVERSION_BEAR_V2" not in src
assert "check_bear_buy_signal_v2" not in src