diff --git a/stock/app/holdings_intel.py b/stock/app/holdings_intel.py index f760026..49a3b0c 100644 --- a/stock/app/holdings_intel.py +++ b/stock/app/holdings_intel.py @@ -152,7 +152,7 @@ def market_events(ticker: str, ticker_prices: "pd.DataFrame", last_vol = vol.iloc[-1] if mu > 0 and ( (sd and (last_vol - mu) / sd >= p["vol_z"]) - or (not sd and last_vol >= mu * p["vol_z"]) # sd=0 fallback: plain ratio + or (not sd and last_vol >= mu * p["vol_z"]) # sd=0 (평탄 기준선): vol_z를 Z-score가 아닌 단순 배수로 사용 ): z_txt = f"{(last_vol - mu) / sd:.1f}" if sd else f"ratio={last_vol / mu:.1f}x" events.append({ diff --git a/stock/app/test_holdings_intel.py b/stock/app/test_holdings_intel.py index 455be8e..30dcee7 100644 --- a/stock/app/test_holdings_intel.py +++ b/stock/app/test_holdings_intel.py @@ -226,3 +226,50 @@ def test_portfolio_health(): assert h["positions"] == 2 assert 0 <= h["max_weight"] <= 1.0 assert "total_eval" in h and "total_pnl" in h and "cash_ratio" in h + + +def test_market_events_volume_surge_zscore_path(): + # 변동 있는 기준선 → Z-score 경로(sd>0) 검증 (sd=0 fallback 아님) + import random as _r + _r.seed(1) + base_vols = [1000 + _r.randint(-50, 50) for _ in range(30)] + closes = [1000] * 30 + [1010] + vols = base_vols + [max(base_vols) * 10] # 마지막날 큰 급증 + df = _ticker_prices(closes, vols) + evts = hi.market_events("005930", df, None, DEFAULT_EVENT) + assert any(e["type"] == "volume_surge" for e in evts) + + +def test_market_events_foreign_selling(): + closes = [1000] * 5 + df = _ticker_prices(closes) + import datetime as _dt + base = _dt.date(2025, 1, 1) + flow = pd.DataFrame({ + "ticker": ["005930"] * 5, + "date": [(base + _dt.timedelta(days=i)).isoformat() for i in range(5)], + "foreign_net": [100, 50, -10, -20, -30], # 최근 3일 연속 순매도 + "institution_net": [0] * 5, + }) + evts = hi.market_events("005930", df, flow, DEFAULT_EVENT) + assert any(e["type"] == "foreign_selling" for e in evts) + + +def test_news_issues_severity_high_boundary(monkeypatch): + monkeypatch.setattr(hi, "_news_sentiment_map", lambda date: { + "005930": {"score_raw": -0.6, "news_count": 5}}) # 정확히 high 경계 + issues = hi.news_issues(["005930"], date="2026-05-29", use_llm=False) + assert issues["005930"][0]["severity"] == "high" + + +def test_portfolio_health_empty_and_zero(): + # 빈 포트 → 0/빈값, 크래시 없음 + h0 = hi.portfolio_health([], total_cash=0) + assert h0["positions"] == 0 + assert h0["max_weight"] == 0.0 + assert h0["total_pnl_rate"] == 0.0 + assert h0["cash_ratio"] == 0.0 + # total_buy=0 (avg_price 0) → div-by-zero 없이 0.0 + h1 = hi.portfolio_health([{"ticker": "X", "quantity": 1, "avg_price": 0, + "current_price": 0, "is_krx": True}], total_cash=0) + assert h1["total_pnl_rate"] == 0.0