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

@@ -606,10 +606,19 @@ def dual_env(monkeypatch):
monkeypatch.setattr(ad, "get_current_adaptive_params",
lambda: SimpleNamespace(fallback_used=False, trading_mode="normal"))
monkeypatch.setattr(te, "_count_swing_buys_today_v2", AsyncMock(return_value=0))
bars = [{"stck_clpr": str(100 + i), "stck_hgpr": str(101 + i), "acml_vol": "1000"}
# [하드닝 R2 2026-10-09] 차트 현재 종가에도 가격 필터(MIN_PRICE_KRW 이하 거부)가 걸리므로
# 가격대를 settings 기준으로 경계 위에 둔다.
_base = int(settings.MIN_PRICE_KRW) * 10
bars = [{"stck_clpr": str(_base + i), "stck_hgpr": str(_base + 1 + i), "acml_vol": "1000"}
for i in range(30)]
api = MagicMock()
api.get_daily_chart = AsyncMock(return_value={"output2": bars})
# [하드닝 R2 2026-10-09] 듀얼 스캔은 네이버 → KIS 기간 조회 → get_daily_chart 순으로
# 일봉을 확보한다. 네이버는 conftest 전역 차단 대상이 아니므로 반드시 목킹한다.
import app.services.naver_finance as nf
monkeypatch.setattr(nf, "get_naver_daily_chart", AsyncMock(return_value=None))
api._quote_delegate = lambda: None
api.get_daily_ohlcv_range = AsyncMock(return_value=[])
monkeypatch.setattr(kia, "korea_investment_api", api)
monkeypatch.setattr(mr, "check_momentum_breakout_signal_v2", lambda **kw: {
"signal": "BUY", "strategy": "MOMENTUM_BREAKOUT", "confidence": 0.7, "description": "t"})
@@ -733,6 +742,22 @@ def _scheduler_env(monkeypatch, mr_side_effect=None):
monkeypatch.setattr(rec_mod, "get_rule_based_recommendations_from_db", AsyncMock(return_value={}))
monkeypatch.setattr(mcp_mod, "request_mcp_risk_assessment", AsyncMock(return_value=(True, "")))
monkeypatch.setattr(redis_mod, "get_json", AsyncMock(return_value=None))
# [하드닝 R2] 기동 시 _check_mdd_threshold_knobs_v2 → strategy_cfg.aget → _refresh_impl →
# AsyncSessionLocal() 이 실제 DB(127.0.0.1:5432)에 접속하고 있었다(소켓 차단 하 실패,
# 로컬 DB 가 떠 있으면 실제 strategy_configs 행에 의존). 예외 발생형으로 막는다:
# - strategy_cfg.aget: RuntimeError → 대조 함수의 "DB 조회 실패(대조 생략)" 분기를 실제로 탄다
# - strategy_config_service.AsyncSessionLocal(공통 종단): 호출되면 기록 + 예외 → 테스트가 0회를 단언
import app.services.strategy_config_service as scs
sessions_opened = []
def _no_db_session(*a, **k):
sessions_opened.append(1)
raise RuntimeError("unit test: strategy_config DB session blocked")
monkeypatch.setattr(scs, "AsyncSessionLocal", _no_db_session)
aget = AsyncMock(side_effect=RuntimeError("unit test: strategy_cfg.aget blocked"))
monkeypatch.setattr(scs.strategy_cfg, "aget", aget)
order_meta = {"sessions_opened": sessions_opened, "aget": aget}
async def _mr(*a, **k):
order.append("mean_reversion")
@@ -744,6 +769,7 @@ def _scheduler_env(monkeypatch, mr_side_effect=None):
monkeypatch.setattr(sch, "_run_recommendation_entry_v2", _mr)
monkeypatch.setattr(sch, "_run_dual_strategy_scan_v2", _dual)
monkeypatch.setattr(sch, "_r2_test_meta", order_meta, raising=False) # 회귀 테스트 단언용
return sch, order
@@ -759,6 +785,25 @@ async def test_scheduler_runs_mean_reversion_then_dual(monkeypatch):
assert order == ["mean_reversion", "dual"]
@pytest.mark.asyncio
async def test_scheduler_startup_mdd_knob_check_opens_no_db_session(monkeypatch):
"""[하드닝 R2] 기동 대조(_check_mdd_threshold_knobs_v2)는 실제로 호출되지만(aget 첫 키에서 예외),
strategy_config DB 세션은 한 번도 열지 않고, 조회 실패가 사이클 진행을 막지 않는다."""
sch, order = _scheduler_env(monkeypatch)
meta = sch._r2_test_meta
monkeypatch.setattr(sch, "_mdd_knob_state", {"mismatches": ["stale"]})
await _one_cycle(sch)
assert order == ["mean_reversion", "dual"]
keys = [c.args[0] for c in meta["aget"].await_args_list]
assert keys == ["MDD_PAUSE_THRESHOLD"] # 첫 조회 예외 → 대조 생략(warning), DB 미접속
assert meta["sessions_opened"] == []
# settings 값만으로 대조가 끝났다 — settings 가 실효값과 같으면 불일치 없음
expected = sch._mdd_threshold_mismatches_v2({"settings": {
"MDD_PAUSE_THRESHOLD": settings.MDD_PAUSE_THRESHOLD,
"MDD_FORCE_THRESHOLD": settings.MDD_FORCE_THRESHOLD}})
assert sch._mdd_knob_state["mismatches"] == expected
@pytest.mark.asyncio
async def test_scheduler_mean_reversion_error_does_not_skip_dual(monkeypatch):
sch, order = _scheduler_env(monkeypatch, mr_side_effect=RuntimeError("boom"))