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,89 @@
"""[하드닝 R8] 추천 경로 매도의 ActiveTrade 대상 역추적.
역추적 SELECT 예외 시 로그 없이 'swing' 이름으로 폴백하면, 실제 행이 'synced' 일 때
close/upsert 매칭이 0건 → 유령 active 행 + remaining_pct 미갱신(분할 단계 재발동).
예외 시에는 warning 을 남기고 비-DT 최신 행 필터(strategy_name_not)로 겨냥해야 한다.
"""
import logging
from types import SimpleNamespace
import pytest
from app.services import trade_executor as te
from app.services.db_worker import DT_STRATEGY_NAMES
class _DBRaises:
async def execute(self, *_a, **_k):
raise RuntimeError("db down")
class _DBReturns:
def __init__(self, row):
self._row = row
async def execute(self, *_a, **_k):
row = self._row
class _R:
def scalars(self_inner):
return SimpleNamespace(first=lambda: row)
return _R()
class _DBMustNotBeCalled:
async def execute(self, *_a, **_k):
raise AssertionError("strategy_name 이 있으면 역추적 SELECT 를 하지 않아야 한다")
@pytest.mark.asyncio
async def test_lookup_exception_targets_non_dt_row_and_warns(caplog):
caplog.set_level(logging.WARNING, logger=te.logger.name)
name, filt = await te._resolve_sell_active_trade_target_v2(
_DBRaises(), {"strategy_type": "swing"}, "323410"
)
assert filt is True, "예외 시 'swing' 이름 폴백 → synced 행 매칭 0건(유령 행)"
assert name is None
assert any("역추적 실패" in r.getMessage() and r.levelno >= logging.WARNING
for r in caplog.records), "예외 흔적이 warning 이상으로 남아야 한다(§4)"
@pytest.mark.asyncio
async def test_lookup_exception_keeps_dt_tagged_holding_on_dt_name():
dt = DT_STRATEGY_NAMES[0]
name, filt = await te._resolve_sell_active_trade_target_v2(
_DBRaises(), {"strategy_type": dt}, "005930"
)
assert (name, filt) == (dt, False)
@pytest.mark.asyncio
async def test_lookup_finds_synced_row_name():
name, filt = await te._resolve_sell_active_trade_target_v2(
_DBReturns(SimpleNamespace(strategy_name="synced")), {}, "005930"
)
assert (name, filt) == ("synced", False)
@pytest.mark.asyncio
async def test_lookup_null_name_row_uses_non_dt_filter():
name, filt = await te._resolve_sell_active_trade_target_v2(
_DBReturns(SimpleNamespace(strategy_name=None)), {"strategy_type": "swing"}, "005930"
)
assert (name, filt) == (None, True)
@pytest.mark.asyncio
async def test_explicit_strategy_name_skips_lookup():
name, filt = await te._resolve_sell_active_trade_target_v2(
_DBMustNotBeCalled(), {"strategy_name": "MOMENTUM_BREAKOUT"}, "005930"
)
assert (name, filt) == ("MOMENTUM_BREAKOUT", False)
def test_execute_sell_uses_resolver():
import inspect
src = inspect.getsource(te._execute_sell)
assert "_resolve_sell_active_trade_target_v2(" in src
assert "_at_filter_not_dt = False\n" not in src.split("_resolve_sell_active_trade_target_v2(")[1][:400]