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:
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user