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

@@ -34,9 +34,13 @@ async def test_dt_full_exit_does_not_zero_swing_shares():
strategy_type="swing",
)]
q = AsyncMock()
with patch("app.services.db_worker.enqueue_portfolio_upsert", q):
# DB 조회는 목: 실제 AsyncSessionLocal(소켓)을 열지 않는다. None = 조회 실패 → 캐시 수량 기준.
db_q = AsyncMock(return_value=None)
with patch("app.services.db_worker.enqueue_portfolio_upsert", q), \
patch.object(bridge, "_db_quantity", db_q):
await bridge.reflect_sell("064400", 7, 70300.0, remaining_qty=0)
db_q.assert_awaited_once_with("064400")
assert portfolio_cache.holdings[0].quantity == 8, "스윙 보유분이 남아야 함"
assert q.await_args.args[0]["quantity"] == 8
@@ -138,7 +142,10 @@ async def test_swing_partial_exit_persists_remaining_quantity():
src = inspect.getsource(ses)
block = src[src.index("분할 매도 후 remaining_pct 갱신"):]
block = block[:block.index("await enqueue_active_trade_upsert(_at_data)")]
assert '"quantity": quantity - sell_qty' in block, "스윙 분할매도에 잔량 갱신 누락"
# [하드닝 R3 2026-10-09] 잔량 = 상한 적용 전 스윙 보유(_swing_basis) − 매도분(_swing_left).
# 동작 검증은 tests/test_swing_hardening_r3_cap_20261009.py.
assert '"quantity": _swing_left' in block, "스윙 분할매도에 잔량 갱신 누락"
assert "_swing_left = max(0, _swing_basis - sell_qty)" in src
def test_us_partial_exit_persists_remaining_quantity():
@@ -347,3 +354,15 @@ def test_sync_seeds_active_trade_for_external_positions():
# 큐잉은 commit 성공 후 — 롤백된 Portfolio 에 ActiveTrade 가 생기면 안 된다
assert src.index("await db.commit()") < src.index("_enq_seed_at("), \
"commit 전에 시딩 큐잉 — 롤백 시 유령 ActiveTrade"
class _DbBlocked(BaseException):
"""AsyncSessionLocal 실사용 차단용(프로덕션 except Exception 에 삼켜지지 않도록 BaseException)."""
def test_bridge_db_quantity_uses_blocked_session_guard():
"""회귀: 목 없이 AsyncSessionLocal 을 열면 즉시 실패해야 한다(소켓 차단 증거)."""
import asyncio
with patch("app.core.database.AsyncSessionLocal", side_effect=_DbBlocked("db blocked")):
with pytest.raises(_DbBlocked):
asyncio.run(bridge._db_quantity("064400"))