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:
89
backend/tests/test_sell_active_trade_target_r8.py
Normal file
89
backend/tests/test_sell_active_trade_target_r8.py
Normal 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]
|
||||
Reference in New Issue
Block a user