From c3a3055060082da20d1c4db61b19264b4c787a4e Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 31 May 2026 22:09:05 +0900 Subject: [PATCH] =?UTF-8?q?test(stock):=20Phase=203=20=EC=BB=A4=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=20=EB=B3=B4=EA=B0=95=20(volume=20Z=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=C2=B7=EC=99=B8=EC=9D=B8=EB=A7=A4=EB=8F=84=C2=B7severi?= =?UTF-8?q?ty=EA=B2=BD=EA=B3=84=C2=B7=EB=B9=88=ED=8F=AC=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- stock/app/holdings_intel.py | 2 +- stock/app/test_holdings_intel.py | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) 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