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:
81
backend/tests/test_news_sentiment_round3_20261009.py
Normal file
81
backend/tests/test_news_sentiment_round3_20261009.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""news_sentiment 라운드3: UTC 기사 나이 정규화, 0건 비캐시, 연속 0건 경보."""
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.timeutil import now_kst
|
||||
from app.services import news_sentiment as ns
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset(monkeypatch):
|
||||
for st in ns._health.values():
|
||||
st.update(consecutive_empty=0, last_success_at=None, alerted=False)
|
||||
# 네트워크/Redis/DB 는 호출 즉시 예외 (목 누락 시 거짓 통과 방지)
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("unmocked external call")
|
||||
monkeypatch.setattr(ns, "get_json", AsyncMock(return_value=None))
|
||||
monkeypatch.setattr(ns, "_save_news_to_db", boom)
|
||||
yield
|
||||
|
||||
|
||||
def test_utc_article_age_is_normalized_to_kst():
|
||||
# 16시간 전 UTC 기사 -> 24h 이내 가중치 1.0 이어야 한다(구버그: 25h -> 0.6)
|
||||
pub = (datetime.now(timezone.utc) - timedelta(hours=16)).isoformat().replace("+00:00", "Z")
|
||||
recent = {"sentiment_score": -1.0, "published_at": pub}
|
||||
fresh = {"sentiment_score": 1.0, "published_at": now_kst()}
|
||||
# 둘 다 가중 1.0 이면 평균 0.0, 구버그면 (-0.6+1)/1.6=0.25
|
||||
assert ns._compute_aggregate_score([recent, fresh]) == 0.0
|
||||
|
||||
|
||||
def test_aware_datetime_object_normalized():
|
||||
aware = datetime.now(timezone.utc) - timedelta(hours=16)
|
||||
out = ns._to_kst_naive(aware)
|
||||
assert out.tzinfo is None
|
||||
assert abs((now_kst() - out).total_seconds() - 16 * 3600) < 5
|
||||
|
||||
|
||||
def test_empty_stock_news_not_cached_and_counted(monkeypatch):
|
||||
setex = AsyncMock()
|
||||
monkeypatch.setattr(ns, "setex_json", setex)
|
||||
monkeypatch.setattr(ns, "collect_stock_news", AsyncMock(return_value=[]))
|
||||
assert asyncio.run(ns.get_stock_news_score("005930")) == 0.0
|
||||
keys = [c.args[0] for c in setex.call_args_list]
|
||||
assert "news_score:stock:005930" not in keys # 0.0 점수 캐시 금지
|
||||
assert ns._health["stock"]["consecutive_empty"] == 1
|
||||
|
||||
|
||||
def test_consecutive_empty_alerts_once(monkeypatch, caplog):
|
||||
from app.services import telegram_notifier as tn
|
||||
send = AsyncMock(return_value=True)
|
||||
monkeypatch.setattr(tn.telegram_notifier, "send_message", send)
|
||||
thr = int(getattr(settings, "NEWS_EMPTY_ALERT_THRESHOLD", ns._EMPTY_ALERT_DEFAULT))
|
||||
|
||||
async def run():
|
||||
for _ in range(thr + 3):
|
||||
await ns._record_collect_result("market", 0)
|
||||
with caplog.at_level("ERROR"):
|
||||
asyncio.run(run())
|
||||
assert send.await_count == 1
|
||||
assert any(r.levelname == "ERROR" for r in caplog.records)
|
||||
|
||||
|
||||
def test_success_resets_and_records_time():
|
||||
asyncio.run(ns._record_collect_result("stock", 0))
|
||||
asyncio.run(ns._record_collect_result("stock", 3))
|
||||
h = ns.get_news_collect_health()["stock"]
|
||||
assert h["consecutive_empty"] == 0 and h["last_success_at"]
|
||||
|
||||
|
||||
def test_stock_news_with_articles_is_cached(monkeypatch):
|
||||
setex = AsyncMock()
|
||||
monkeypatch.setattr(ns, "setex_json", setex)
|
||||
monkeypatch.setattr(ns, "_save_news_to_db", AsyncMock())
|
||||
arts = [{"title": "어닝쇼크", "sentiment_score": -0.9, "published_at": now_kst()}]
|
||||
monkeypatch.setattr(ns, "collect_stock_news", AsyncMock(return_value=arts))
|
||||
score = asyncio.run(ns.get_stock_news_score("005930"))
|
||||
assert score <= settings.NEWS_BLOCK_SCORE
|
||||
setex.assert_awaited_with("news_score:stock:005930", settings.NEWS_CACHE_TTL, score)
|
||||
Reference in New Issue
Block a user