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