Files
auto-stock/backend/tests/test_regime_kis_fallback_r3.py
shuim a264561576 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
2026-10-10 12:17:49 +09:00

102 lines
3.6 KiB
Python

from datetime import datetime
import pytest
from app.services import market_analyzer as ma
def _rows(n):
return [{"stck_clpr": str(100 + i * 0.1), "stck_hgpr": str(101 + i * 0.1),
"stck_lwpr": str(99 + i * 0.1), "acml_vol": "1000"} for i in range(n)][::-1]
class _Api:
def __init__(self, rng, chart):
self.rng, self.chart, self.calls = rng, chart, []
async def get_daily_ohlcv_range(self, *a):
self.calls.append("range")
if isinstance(self.rng, Exception):
raise self.rng
return self.rng
async def get_daily_chart(self, *a, **k):
self.calls.append("chart")
return {"output2": self.chart}
@pytest.fixture(autouse=True)
def _setup(monkeypatch):
saved = dict(ma._market_regime_cache)
ma._market_regime_cache.update(result=None, ts=0.0)
monkeypatch.setattr(ma, "is_market_open", lambda: True)
monkeypatch.setattr(ma.settings, "TRADING_HALTED", True, raising=False)
async def _naver_fail(*a, **k):
raise RuntimeError("naver down")
import app.services.naver_finance as nf
monkeypatch.setattr(nf, "get_naver_daily_chart", _naver_fail)
yield
ma._market_regime_cache.update(saved)
@pytest.mark.asyncio
async def test_kis_range_fallback_reaches_205(monkeypatch):
api = _Api(_rows(250), _rows(30))
monkeypatch.setattr(ma, "_real_market_api", api)
r = await ma.check_market_regime()
assert r["regime"] == "halted" # 데이터 부족 분기를 통과해 TRADING_HALTED 까지 도달
assert api.calls == ["range"]
assert ma.get_regime_source_health()["source"] == "kis_range"
@pytest.mark.asyncio
async def test_short_data_blocks_with_error_log(monkeypatch, caplog):
api = _Api(RuntimeError("kis down"), _rows(30))
monkeypatch.setattr(ma, "_real_market_api", api)
caplog.set_level("ERROR")
r = await ma.check_market_regime()
assert r["max_trades"] == 0
assert any("신규 진입 차단" in m for m in caplog.messages)
h = ma.get_regime_source_health()
assert h["blocked"] is True and h["bars"] == 30
def _dated(rows, date):
out = [dict(r) for r in rows]
for i, r in enumerate(out):
r["stck_bsop_date"] = date if i == 0 else "20200101"
return out
@pytest.mark.asyncio
async def test_stale_naver_falls_to_kis_fresh(monkeypatch):
import app.services.naver_finance as nf
monkeypatch.setattr(ma, "_get_kst_now", lambda: datetime(2026, 10, 9, 10, 0))
old = list(reversed(_dated(_rows(250), "20261008"))) # oldest-first, 최신봉=전일
async def _naver(*a, **k):
return old
monkeypatch.setattr(nf, "get_naver_daily_chart", _naver)
api = _Api(_dated(_rows(250), "20261009"), _rows(30))
monkeypatch.setattr(ma, "_real_market_api", api)
await ma.check_market_regime()
h = ma.get_regime_source_health()
assert h["source"] == "kis_range" and h["stale"] is False
assert api.calls == ["range"]
@pytest.mark.asyncio
async def test_all_sources_stale_warns_and_flags(monkeypatch, caplog):
import app.services.naver_finance as nf
monkeypatch.setattr(ma, "_get_kst_now", lambda: datetime(2026, 10, 9, 10, 0))
old = list(reversed(_dated(_rows(250), "20261008")))
async def _naver(*a, **k):
return old
monkeypatch.setattr(nf, "get_naver_daily_chart", _naver)
monkeypatch.setattr(ma, "_real_market_api", _Api(_dated(_rows(250), "20261008"), _rows(30)))
caplog.set_level("WARNING")
r = await ma.check_market_regime()
assert r["regime"] == "halted"
assert ma.get_regime_source_health()["stale"] is True
assert any("낡음" in m for m in caplog.messages)